diff --git a/CHANGELOG.md b/CHANGELOG.md index d5866c1..6de762a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v2026.07.15 + +- Adds the Task Contract Gate so hard tasks define done, required proof, reject conditions, and gate status before an answer is accepted as complete. +- Adds contract-aware response scorecards and live `/api/run` payload fields for task contracts, deliverables, assumptions, scorecards, and gate failures. +- Adds a 50-question Fusion & Orca Samples regression pack covering Fusion 360 CAD/CAM workflows, OrcaSlicer Codex app/preset repair, Orca profile workflow, and Orca filament calibration. +- Fixes Fusion 360 user-parameter prompts being mistaken for Orca pressure-advance or local slicer-profile pulls. +- Fixes conceptual Orca filament/profile questions so they answer the workflow directly instead of dumping unrelated local profile data. +- Runs and verifies the full local regression gauntlet: 12 hard cases, 50 Fusion/Orca samples, 100 manufacturing samples, 56 domain samples, and the TinmanX1 Polymaker workflow scenario. + ## v2026.07.14 - Adds the 3D Printing Expert Pack with printer profiles for Bambu H2D/X1C, Creality K2 Plus, Qidi Plus 4, Snapmaker U1, Rat Rig V-Core 4.1 IDEX Klipper, Sovol SV08 Max, and ELEGOO Centauri Carbon. diff --git a/Codex_CLI_UI/README.md b/Codex_CLI_UI/README.md index b90a194..f260a46 100644 --- a/Codex_CLI_UI/README.md +++ b/Codex_CLI_UI/README.md @@ -71,8 +71,6 @@ The server binds to `127.0.0.1` by default and streams `codex exec --json` outpu - The right rail includes a live `Model Health` graph for Ollama, model stack, Manager pass timing, memory, disk, load, and Qidi reachability. - Active runs show concise `Working notes` in the assistant message while Codex is thinking and using tools. - The `Admin` screen includes topic cleanup, stable knowledge Promote/Delete controls, the Improvement Lab, saved golden-test counts, model warmup, performance benchmark runs, and a package health check. -- The `Admin` screen also includes a 3D Printing Expert Pack card. It shows printer/material/source-vault coverage and can refresh official/manual sources into `data/source-vault/3d-printing`. -- Printer and filament questions use the built-in printer profiles, material library, OrcaSlicer calibration workflow, and cached component docs before falling back to general research. Terminal shortcuts: @@ -90,6 +88,46 @@ python3 import_codex_history.py This creates a compact local index in `data/` from `~/.codex/sessions`. The UI server injects relevant history snippets into new Codex runs. +## Harvest Private History Tests + +```bash +cd "$HOME/Applications/Codex_CLI_UI" +python3 harvest_history_golden_tests.py --limit 120 +``` + +This scans local Codex sessions and archived sessions, sanitizes obvious secrets, deduplicates user prompts, and writes private Slow-group golden tests into `data/golden_tests.json`. The public package includes the harvester only; it does not bundle personal chat history or generated private tests. + +Run a safe local batch through the live UI server: + +```bash +python3 run_golden_batch.py --limit 12 +python3 run_golden_batch.py --offset 12 --limit 12 +``` + +This selects private history-harvest tests that do not require live web, GitHub, credentials, downloads, or live printer actions, then records the batch result under `data/golden_batch_results/`. + +Run Tinman's curated engineering-domain sample set: + +```bash +python3 run_golden_batch.py --group "Domain Samples" --source domain-sample --limit 56 +``` + +The Domain Samples group covers 3D printing, CNC machining, solar/wind power, aerodynamics, CFD, engineering, and aviation. These are public-safe built-in guardrails, not private chat-history exports. + +Run the public CAD/CNC manufacturing question bank: + +```bash +python3 run_golden_batch.py --group "Manufacturing Samples" --source manufacturing-sample --limit 100 +``` + +Run the TinmanX1/Codex UI workflow scenario directly: + +```bash +python3 run_golden_batch.py --ids scenario-tinmanx1-polymaker-steer-self-repair-release +``` + +The Manufacturing Samples group covers 50 CAD and 50 CNC machining prompts. The workflow scenario guards the Polymaker/Fiberon preset, Steer/Edit UI, self-healing Fix-this loop, GitHub release, and zip packaging behavior. + ## Hybrid Cloud Research Setup The official OpenAI CLI is installed with Homebrew: diff --git a/Codex_CLI_UI/app.js b/Codex_CLI_UI/app.js index a5ff89d..efbb96e 100644 --- a/Codex_CLI_UI/app.js +++ b/Codex_CLI_UI/app.js @@ -124,6 +124,7 @@ let config = { }; let activeController = null; let pendingAttachments = []; +let composerIntent = { kind: "", messageId: "" }; const testBench = { running: false, activeId: "", @@ -1305,6 +1306,9 @@ function renderMessages() { answer.className = "answer-text"; renderMessageText(answer, message); body.appendChild(answer); + if (message.role === "user" && !message.running && String(message.text || "").trim()) { + body.appendChild(buildUserMessageActions(message)); + } const responsePackage = buildResponsePackagePanel(message); if (responsePackage) body.appendChild(responsePackage); const thoughts = buildThoughtsCard(message); @@ -1353,6 +1357,7 @@ function buildResponsePackagePanel(message) { const deliverables = responsePackageItems(message, "deliverables"); const assumptions = responsePackageItems(message, "assumptions"); const contract = message.taskContract || null; + const contractGate = message.contractGate || message.scorecard?.contractGate || null; const roleStyle = message.roleStyle || null; const scorecard = message.scorecard || null; if (!deliverables.length && !assumptions.length && !contract && !roleStyle && !scorecard) return null; @@ -1403,10 +1408,13 @@ function buildResponsePackagePanel(message) { const summary = document.createElement("summary"); const summaryTitle = document.createElement("span"); summaryTitle.className = "answer-check-title"; - summaryTitle.textContent = "Answer check"; + summaryTitle.textContent = "Task contract"; const score = document.createElement("span"); score.className = `score-pill ${scorecard?.status || "pass"}`; - score.textContent = Number.isFinite(scorecard?.score) ? `${scorecard.score}%` : "Ready"; + const gateText = contractGate?.status ? `${contractGate.status}` : ""; + score.textContent = Number.isFinite(scorecard?.score) + ? `${scorecard.score}%${gateText ? ` · ${gateText}` : ""}` + : gateText || "Ready"; summary.append(summaryTitle, score); details.appendChild(summary); @@ -1415,6 +1423,16 @@ function buildResponsePackagePanel(message) { grid.className = "answer-check-grid"; if (contract?.kind) grid.appendChild(buildAnswerCheckFact("Task", contract.kind)); if (contract?.doneMeans) grid.appendChild(buildAnswerCheckFact("Done means", contract.doneMeans)); + if (Array.isArray(contract?.mustDo) && contract.mustDo.length) { + grid.appendChild(buildAnswerCheckFact("Must do", contract.mustDo.join(", "))); + } + if (Array.isArray(contract?.requiredProof) && contract.requiredProof.length) { + grid.appendChild(buildAnswerCheckFact("Required proof", contract.requiredProof.join(", "))); + } + if (Array.isArray(contract?.rejectIf) && contract.rejectIf.length) { + grid.appendChild(buildAnswerCheckFact("Reject if", contract.rejectIf.slice(0, 4).join(", "))); + } + if (contractGate?.status) grid.appendChild(buildAnswerCheckFact("Gate", contractGate.status)); if (contract?.role || roleStyle?.title) grid.appendChild(buildAnswerCheckFact("Role", contract?.role || roleStyle.title)); if (roleStyle?.voice) grid.appendChild(buildAnswerCheckFact("Voice", roleStyle.voice)); if (Array.isArray(roleStyle?.checklist) && roleStyle.checklist.length) { @@ -1466,6 +1484,19 @@ function buildResponsePackagePanel(message) { return wrap.childElementCount ? wrap : null; } +function buildUserMessageActions(message) { + const actions = document.createElement("div"); + actions.className = "message-actions"; + const edit = document.createElement("button"); + edit.className = "message-action-button"; + edit.type = "button"; + edit.dataset.editMessageId = message.id; + edit.textContent = "Edit question"; + edit.disabled = Boolean(activeController); + actions.appendChild(edit); + return actions; +} + function buildAnswerCheckFact(label, value) { const item = document.createElement("div"); item.className = "answer-check-fact"; @@ -1487,7 +1518,7 @@ function buildFeedbackActions(message) { } else if (message.feedback === "good") { status.textContent = "Marked good"; } else if (message.feedback === "fix") { - status.textContent = message.feedbackGoldenTest ? "Lesson saved + test" : "Lesson saved"; + status.textContent = message.feedbackSelfHealing ? "Lesson saved + self-heal" : message.feedbackGoldenTest ? "Lesson saved + test" : "Lesson saved"; } else if (message.feedback === "error") { status.textContent = "Feedback not saved"; } @@ -1508,7 +1539,14 @@ function buildFeedbackActions(message) { fix.textContent = "Fix this"; fix.disabled = message.feedback === "saving"; - actions.append(good, fix); + const steer = document.createElement("button"); + steer.className = "feedback-button"; + steer.type = "button"; + steer.dataset.steerMessageId = message.id; + steer.textContent = "Steer"; + steer.disabled = message.feedback === "saving"; + + actions.append(good, fix, steer); if (status.textContent) actions.appendChild(status); return actions; } @@ -2855,8 +2893,21 @@ function workingIntroForPrompt(text, attachments = []) { async function sendPrompt() { const thread = currentThread(); const text = els.promptInput.value.trim(); - const attachments = pendingAttachments.slice(); + let attachments = pendingAttachments.slice(); if (!thread || (!text && !attachments.length) || activeController) return; + let editingIndex = -1; + let editingMessage = null; + if (composerIntent.kind === "edit" && composerIntent.messageId) { + editingIndex = findMessageIndexById(thread, composerIntent.messageId); + editingMessage = editingIndex >= 0 ? thread.messages[editingIndex] : null; + if (editingMessage?.role !== "user") { + editingIndex = -1; + editingMessage = null; + } + if (editingMessage && !attachments.length && Array.isArray(editingMessage.attachments)) { + attachments = editingMessage.attachments.slice(); + } + } thread.cwd = els.cwdInput.value.trim() || config.cwd; thread.profile = thread.profile || config.profile; @@ -2866,12 +2917,16 @@ async function sendPrompt() { thread.friendlinessLevel = normalizeFriendliness(thread.friendlinessLevel || config.friendlinessLevel); thread.humorLevel = normalizeHumor(thread.humorLevel || config.humorLevel); thread.webSearch = normalizeWebSearch(thread.webSearch || config.webSearch); + if (editingIndex >= 0) { + thread.messages = thread.messages.slice(0, editingIndex); + } thread.messages.push({ id: crypto.randomUUID(), role: "user", text, attachments }); if (isUntitledThread(thread)) { thread.title = (text || attachments[0]?.name || "Attached file").split(/\s+/).slice(0, 7).join(" "); } thread.updatedAt = new Date().toISOString(); pendingAttachments = []; + composerIntent = { kind: "", messageId: "" }; els.promptInput.value = ""; autoSizeTextarea(); renderAttachmentTray(); @@ -3001,6 +3056,7 @@ async function runDeeperAnalysis(kind = "auto") { pending.deliverables = Array.isArray(payload.deliverables) ? payload.deliverables : []; pending.assumptions = Array.isArray(payload.assumptions) ? payload.assumptions : []; pending.scorecard = payload.scorecard || null; + pending.contractGate = payload.contractGate || null; pending.thoughts = Array.isArray(payload.thoughts) && payload.thoughts.length ? payload.thoughts : pending.thoughts; @@ -3172,6 +3228,9 @@ async function runGoldenTest(test, signal) { warnings: [], logs: [], analyticalCore: null, + taskContract: null, + contractGate: null, + scorecard: null, }; const response = await fetch("/api/run", { method: "POST", @@ -3202,6 +3261,9 @@ async function runGoldenTest(test, signal) { if (event.type === "assistant") { run.answer = event.text || ""; run.analyticalCore = event.analyticalCore || null; + run.taskContract = event.taskContract || null; + run.contractGate = event.contractGate || null; + run.scorecard = event.scorecard || null; } if (event.type === "thought") run.thoughts.push(event.text || ""); if (event.type === "warning" || event.type === "error") run.warnings.push(event.text || event.type); @@ -3236,6 +3298,9 @@ function evaluateGoldenTest(test, run) { const answer = String(run.answer || "").trim(); const lower = answer.toLowerCase(); const route = run.route || {}; + const taskContract = run.taskContract || {}; + const contractGate = run.contractGate || {}; + const scorecard = run.scorecard || {}; const checks = []; checks.push({ @@ -3324,6 +3389,41 @@ function evaluateGoldenTest(test, run) { }); } + if (test.expectedContractKind) { + checks.push({ + label: "contract-kind", + passed: taskContract.kind === test.expectedContractKind, + detail: `Expected ${test.expectedContractKind}; got ${taskContract.kind || "none"}.`, + }); + } + + if (test.expectedContractGate) { + checks.push({ + label: "contract-gate", + passed: contractGate.status === test.expectedContractGate, + detail: `Expected ${test.expectedContractGate}; got ${contractGate.status || "none"}.`, + }); + } + + if (test.requiredContractProof?.length) { + const proofText = (taskContract.requiredProof || []).join(" ").toLowerCase(); + const missing = test.requiredContractProof.filter((term) => !proofText.includes(term.toLowerCase())); + checks.push({ + label: "contract-proof", + passed: missing.length === 0, + detail: missing.length ? `Missing: ${missing.join(", ")}` : "Contract proof terms found.", + }); + } + + if (test.minScorecard) { + const score = Number(scorecard.score || 0); + checks.push({ + label: "scorecard", + passed: score >= Number(test.minScorecard), + detail: `Expected response scorecard >= ${test.minScorecard}; got ${score || "none"}.`, + }); + } + const passed = checks.every((check) => check.passed); return { status: passed ? "pass" : "fail", @@ -3333,6 +3433,9 @@ function evaluateGoldenTest(test, run) { route, returnCode: run.returnCode, analyticalCore: run.analyticalCore, + taskContract: run.taskContract, + contractGate: run.contractGate, + scorecard: run.scorecard, thoughts: run.thoughts, warnings: run.warnings, }; @@ -3355,6 +3458,7 @@ function handleEvent(event, pending) { pending.deliverables = Array.isArray(event.deliverables) ? event.deliverables : []; pending.assumptions = Array.isArray(event.assumptions) ? event.assumptions : []; pending.scorecard = event.scorecard || null; + pending.contractGate = event.contractGate || null; renderMessages(); return; } @@ -3449,6 +3553,39 @@ function findMessageById(thread, id) { return (thread.messages || []).find((message) => message.id === id); } +function findMessageIndexById(thread, id) { + return (thread.messages || []).findIndex((message) => message.id === id); +} + +function setComposerText(text) { + els.promptInput.value = text; + autoSizeTextarea(); + els.promptInput.focus(); +} + +function startEditMessage(messageId) { + const thread = currentThread(); + const index = findMessageIndexById(thread, messageId); + const message = index >= 0 ? thread.messages[index] : null; + if (!message || message.role !== "user") return; + composerIntent = { kind: "edit", messageId }; + setComposerText(message.text || ""); + els.runState.textContent = "Editing question"; + els.runState.className = "run-state warning"; + appendLog("event", "editing earlier question; next send reruns from that point"); +} + +function startSteerMessage(messageId) { + const thread = currentThread(); + const message = findMessageById(thread, messageId); + if (!message || message.role !== "assistant") return; + composerIntent = { kind: "steer", messageId }; + setComposerText("Steer the previous answer this way: "); + els.runState.textContent = "Steer ready"; + els.runState.className = "run-state warning"; + appendLog("event", "steer prompt prepared for the previous answer"); +} + function latestUserPromptForMessage(thread, message) { const index = (thread.messages || []).indexOf(message); for (let cursor = index - 1; cursor >= 0; cursor -= 1) { @@ -3500,6 +3637,9 @@ async function sendMessageFeedback(messageId, rating) { note, prompt: latestUserPromptForMessage(thread, message), answer: message.text || "", + messages: thread.messages.filter((item) => !item.running).slice(-8), + cwd: thread.cwd || config.cwd, + webSearch: thread.webSearch || config.webSearch, route: message.route || {}, projectId: message.route?.projectId || message.adminTopic?.projectId || "general", }), @@ -3510,6 +3650,7 @@ async function sendMessageFeedback(messageId, rating) { message.feedback = rating; message.feedbackNote = note; message.feedbackGoldenTest = payload.goldenTest || null; + message.feedbackSelfHealing = payload.selfHealing?.event?.patchQueued || payload.selfHealing?.event || null; if (payload.goldenTests) config.goldenTests = payload.goldenTests; if (payload.admin) config.admin = payload.admin; if (payload.goldenTest) appendLog("status", `Regression test saved: ${payload.goldenTest.name || payload.goldenTest.id}`); @@ -3696,6 +3837,18 @@ els.conversation.addEventListener("click", (event) => { openLocalPath(localPathLink.dataset.localPath); return; } + const editButton = event.target.closest("[data-edit-message-id]"); + if (editButton && !activeController) { + event.preventDefault(); + startEditMessage(editButton.dataset.editMessageId); + return; + } + const steerButton = event.target.closest("[data-steer-message-id]"); + if (steerButton && !activeController) { + event.preventDefault(); + startSteerMessage(steerButton.dataset.steerMessageId); + return; + } const button = event.target.closest("[data-feedback-id]"); if (!button || activeController) return; sendMessageFeedback(button.dataset.feedbackId, button.dataset.feedbackRating); diff --git a/Codex_CLI_UI/harvest_history_golden_tests.py b/Codex_CLI_UI/harvest_history_golden_tests.py new file mode 100755 index 0000000..a9558d6 --- /dev/null +++ b/Codex_CLI_UI/harvest_history_golden_tests.py @@ -0,0 +1,413 @@ +#!/usr/bin/env python3 +import argparse +import hashlib +import json +import re +import time +from datetime import datetime, timezone +from pathlib import Path + +import import_codex_history + + +APP_DIR = Path(__file__).resolve().parent +DATA_DIR = APP_DIR / "data" +GOLDEN_TESTS_PATH = DATA_DIR / "golden_tests.json" +HISTORY_TEST_SUMMARY_PATH = DATA_DIR / "history_golden_test_harvest.json" +SESSIONS_DIR = Path.home() / ".codex" / "sessions" +ARCHIVED_SESSIONS_DIR = Path.home() / ".codex" / "archived_sessions" + +MAX_PROMPT_CHARS = 900 +DEFAULT_LIMIT = 80 + +SENSITIVE_PATTERNS = ( + (re.compile(r"(?i)(password|passwd|pwd|token|api[_-]?key|secret)\s*[:=]\s*['\"]?[^\\s,'\"]+"), r"\1=[REDACTED]"), + (re.compile(r"(?i)(bearer|sk-[a-z0-9_-]{8,})[a-z0-9._-]*"), "[REDACTED_TOKEN]"), + (re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.S), "[REDACTED_PRIVATE_KEY]"), +) + +SKIP_PREFIXES = ( + "", + "", + "automation:", + "automation id:", + "run a metered", + "assistant style:", + "debian gnu/linux comes with absolutely no warranty", +) + +QUESTION_STARTERS = ( + "can you", + "will you", + "would you", + "what", + "how", + "why", + "where", + "when", + "which", + "please", + "i need", + "lets", + "let's", + "fix", + "diagnose", + "search", + "compare", + "design", + "create", + "make", + "write", + "upload", + "update", + "install", + "look at", +) + +ARTIFACT_OR_CHANGE_TERMS = ( + "create", + "make", + "write", + "build", + "generate", + "update", + "fix", + "upload", + "save", + "install", + "download", + "pull", + "set up", +) + +BASE_FORBIDDEN_TERMS = [ + "run failed", + "no final message returned", + "no response", + "load failed", + "recovery plan:", + "i do not have access", + "i don't have access", + "you can check it yourself", +] + + +def compact(text, limit=MAX_PROMPT_CHARS): + text = re.sub(r"\s+", " ", str(text or "")).strip() + if len(text) <= limit: + return text + return text[: limit - 12].rstrip() + " [truncated]" + + +def redact(text): + clean = str(text or "") + if "Public web context:" in clean: + clean = clean.split("Public web context:", 1)[0].rstrip() + for pattern, replacement in SENSITIVE_PATTERNS: + clean = pattern.sub(replacement, clean) + return clean + + +def normalized_key(text): + text = redact(text).lower() + text = re.sub(r"`[^`]+`", " ", text) + text = re.sub(r"/users/[^\s]+", " /path ", text) + text = re.sub(r"\b\d{1,3}(?:\.\d{1,3}){3}\b", " ip ", text) + text = re.sub(r"[^a-z0-9]+", " ", text) + return re.sub(r"\s+", " ", text).strip() + + +def test_id_for_prompt(prompt): + digest = hashlib.sha1(normalized_key(prompt).encode("utf-8")).hexdigest()[:14] + return f"history-{digest}" + + +def is_testable_prompt(text): + stripped = compact(text, 2000) + lower = stripped.lower() + if not stripped or len(stripped) < 14: + return False + if lower in {"are you there?", "thank you", "thanks", "awesome", "perfect", "lets do it", "let's do it"}: + return False + if re.match(r"^[a-z_][a-z0-9_-]*@[^:]+[:$]", lower) and not any(lower.startswith(starter) for starter in QUESTION_STARTERS): + return False + if lower.startswith("failed: traceback") and "can you" not in lower and "will you" not in lower: + return False + if lower.startswith("-") and "?" not in lower: + return False + if re.match(r"^\d{1,2}:\d{2}\s*(?:am|pm)\s+", lower): + return False + if lower.startswith("done. http") and "?" not in lower: + return False + if "translated report" in lower[:120] and "exception type:" in lower and "crashed thread:" in lower: + return False + if "private startup inventory:" in lower or "current request analysis:" in lower: + return False + if any(lower.startswith(prefix) for prefix in SKIP_PREFIXES): + return False + if any(term in lower for term in ("see attached", "see photos", "see photo", "attached image", "attached is", "based on the image", "from this image")): + return False + if re.search(r"\b(?:img[_-]?\d+|screenshot|photo)\.(?:jpe?g|png|heic|webp)\b", lower): + return False + if lower.startswith("# files mentioned by the user:") and "my request for codex:" not in lower: + return False + if len(stripped) < 170 and any(term in lower for term in ("before we move on", "fix this properly")): + return False + if lower.count("\n") > 35 and "my request for codex:" not in lower: + return False + if len(stripped) > 5000: + return False + return "?" in stripped or any(lower.startswith(starter) for starter in QUESTION_STARTERS) + + +def quality_score(text): + lower = text.lower() + score = 0 + if "?" in text: + score += 8 + if len(text) >= 60: + score += 8 + if len(text) >= 160: + score += 6 + domain_terms = ( + "qidi", "rat rig", "ratrig", "klipper", "moonraker", "marlin", "orca", + "filament", "cad", "fusion", "stl", "cfd", "fea", "wiring", "diagram", + "solar", "battery", "codex cli ui", "github", "flight ops", "tinmanx", + "xfoil", "openvsp", "su2", "qblade", "graphviz", + ) + score += min(40, sum(6 for term in domain_terms if term in lower)) + if any(term in lower for term in ("not acceptable", "failure", "failed", "didnt", "didn't", "fix this", "your son")): + score += 18 + if any(term in lower for term in ("search the web", "current", "latest", "price", "availability")): + score += 10 + if any(term in lower for term in ("thank", "awesome", "perfect", "brother")) and len(text) < 80: + score -= 20 + return score + + +def project_for_prompt(text): + lower = text.lower() + if any(term in lower for term in ("codex cli ui", "codex cli", "ollama", "heartbeat", "github", "dock", "app icon")): + return "codex-cli-ui-local-agent" + if any(term in lower for term in ("wiring diagram", "block diagram", "electrical diagram", "schematic", "power diagram", "graphviz")): + return "engineering-diagrams" + if "cpap hose" in lower and any(term in lower for term in ("inner diameter", "id", "inside diameter")): + return "research-parts-reference" + if any(term in lower for term in ("wind turbine", "alternator", "generator", "60vdc", "60 vdc", "300 rpm", "solar", "battery", "charge controller", "inverter", "off-grid", "3 phase", "split phase")): + return "energy-power-research" + if any(term in lower for term in ("tinmanx", "rocket slicer", "orca", "filament profile", "temp tower", "pressure advance", "pet-cf", "pctg", "slicer", "tinmanx1")): + return "tinmanx-slicer-research" + if any(term in lower for term in ("fusion", "freecad", "cad", "stl", "step", "cpap duct", "cooling duct", "fea", "structural", "xfoil", "openvsp", "su2", "qblade")): + return "cad-modeling-projects" + if any(term in lower for term in ("qidi", "rat rig", "ratrig", "klipper", "moonraker", "marlin", "prusa", "bambu", "sovol", "centauri", "printer")): + return "printer-klipper-ops" + if any(term in lower for term in ("price", "availability", "buy", "compare pricing", "generator", "alternator", "part number")): + return "research-parts-reference" + if any(term in lower for term in ("flight ops", "flightops", "pilot", "aircraft", "certificate", "customer", "service charge", "service fee", "services charge", "services charges", "wb air")): + return "flightops-tracker" + return "" + + +def web_needed(text): + lower = text.lower() + return any(term in lower for term in ("search the web", "current", "latest", "today", "price", "availability", "github", "download", "manual")) + + +def required_terms_for_prompt(text, project_id=""): + lower = text.lower() + required = [] + cpap_hose_spec = "cpap hose" in lower and any(term in lower for term in ("inner diameter", "id", "inside diameter")) + artifact_or_change = any(term in lower for term in ARTIFACT_OR_CHANGE_TERMS) + if not artifact_or_change and any(term in lower for term in ("best", "recommend", "should i", "what is the best")): + required.extend(["this is why", "you should also consider"]) + if project_id == "tinmanx-slicer-research" and ("filament" in lower or "temp tower" in lower): + required.append("filament") + if "marlin" in lower: + required.append("marlin") + if "klipper" in lower: + required.append("klipper") + if "fusion" in lower and not cpap_hose_spec: + required.append("fusion") + if "cfd" in lower: + required.append("cfd") + if "web" in lower or "price" in lower or "availability" in lower: + required.append("http") + return list(dict.fromkeys(required))[:6] + + +def golden_test_from_prompt(prompt, source): + prompt = compact(redact(prompt), MAX_PROMPT_CHARS) + project_id = project_for_prompt(prompt) + test = { + "id": test_id_for_prompt(prompt), + "name": compact(prompt, 58), + "group": "Slow", + "prompt": prompt, + "profile": "manager", + "managerDepth": "fast", + "webSearch": "live" if web_needed(prompt) else "disabled", + "expectedProjectId": project_id, + "directAnswer": bool(re.match(r"(?i)^(what|which|can you tell|what is the best|how can|how do)", prompt)), + "directTerms": [], + "requiredTerms": required_terms_for_prompt(prompt, project_id=project_id), + "anyTerms": [], + "forbiddenTerms": BASE_FORBIDDEN_TERMS, + "requiresSource": bool(web_needed(prompt)), + "minAnalyticalScore": 74, + "goal": "Real chat-history regression: answer Tinman's actual prompt without cold fallback, wrong routing, or fake completion.", + "source": "history-harvest", + "historySource": source, + "qualityScore": quality_score(prompt), + "createdAt": time.time(), + "updatedAt": time.time(), + } + if not test["expectedProjectId"]: + test.pop("expectedProjectId") + if not test["requiredTerms"]: + test.pop("requiredTerms") + if not test["requiresSource"]: + test.pop("requiresSource") + return test + + +def session_paths(): + paths = [] + if SESSIONS_DIR.exists(): + paths.extend(SESSIONS_DIR.glob("**/*.jsonl")) + if ARCHIVED_SESSIONS_DIR.exists(): + paths.extend(ARCHIVED_SESSIONS_DIR.glob("*.jsonl")) + return sorted(set(paths)) + + +def split_embedded_user_turns(text): + text = str(text or "") + if not re.search(r"(?m)^User:\s*", text) or not re.search(r"(?m)^Assistant:\s*", text): + return [text] + turns = [] + for match in re.finditer(r"(?ms)^User:\s*(.*?)(?=^Assistant:|^User:|\Z)", text): + chunk = match.group(1).strip() + if chunk: + turns.append(chunk) + return turns or [text] + + +def iter_user_messages(path): + session_id = path.stem + try: + handle = path.open("r", encoding="utf-8", errors="replace") + except OSError: + return + with handle: + for line in handle: + try: + item = json.loads(line) + except json.JSONDecodeError: + continue + item_type = item.get("type") + payload = item.get("payload") or {} + if item_type == "session_meta": + session_id = payload.get("id") or payload.get("session_id") or session_id + continue + if item_type != "response_item" or payload.get("type") != "message" or payload.get("role") != "user": + continue + text = import_codex_history.normalize_text(import_codex_history.extract_text(payload.get("content"))) + if import_codex_history.is_noise(text): + continue + for turn in split_embedded_user_turns(text): + yield turn, session_id + + +def prompt_candidates(): + for path in session_paths(): + for text, session_id in iter_user_messages(path): + if is_testable_prompt(text): + yield text, f"{path.name}:{session_id}" + + +def load_golden_data(): + if GOLDEN_TESTS_PATH.exists(): + try: + data = json.loads(GOLDEN_TESTS_PATH.read_text(encoding="utf-8")) + if isinstance(data, dict): + data.setdefault("version", 1) + data.setdefault("createdAt", time.time()) + data.setdefault("tests", []) + return data + except Exception: + pass + now = time.time() + return {"version": 1, "createdAt": now, "updatedAt": now, "tests": []} + + +def harvest(limit=DEFAULT_LIMIT, dry_run=False, replace_history=False): + seen = set() + tests = [] + for prompt, source in prompt_candidates(): + key = normalized_key(prompt) + if not key or key in seen: + continue + seen.add(key) + tests.append(golden_test_from_prompt(prompt, source)) + + data = load_golden_data() + removed = 0 + if replace_history: + original_count = len(data.get("tests", [])) + data["tests"] = [item for item in data.get("tests", []) if item.get("source") != "history-harvest"] + removed = original_count - len(data["tests"]) + existing = {item.get("id"): item for item in data.get("tests", []) if item.get("id")} + tests.sort(key=lambda item: (-int(item.get("qualityScore") or 0), item.get("expectedProjectId", ""), item["name"].lower())) + new_tests = [item for item in tests if item["id"] not in existing] + old_tests = [item for item in tests if item["id"] in existing] + selected = (new_tests + old_tests)[: max(0, int(limit or 0))] + added = 0 + updated = 0 + for test in selected: + old = existing.get(test["id"]) + if old: + created = old.get("createdAt") or test["createdAt"] + old.update(test) + old["createdAt"] = created + old["updatedAt"] = time.time() + updated += 1 + else: + data["tests"].append(test) + existing[test["id"]] = test + added += 1 + + data["tests"] = sorted(data.get("tests", []), key=lambda item: (item.get("group", ""), item.get("name", ""))) + data["updatedAt"] = time.time() + + summary = { + "harvestedAt": datetime.now(timezone.utc).isoformat(), + "sessionFiles": len(session_paths()), + "candidatePrompts": len(tests), + "selectedTests": len(selected), + "added": added, + "updated": updated, + "removedHistoryTests": removed, + "dryRun": dry_run, + "goldenTestsPath": str(GOLDEN_TESTS_PATH), + "summaryPath": str(HISTORY_TEST_SUMMARY_PATH), + } + if selected: + summary["sampleTests"] = [{"id": item["id"], "name": item["name"], "project": item.get("expectedProjectId", "")} for item in selected[:10]] + + if not dry_run: + DATA_DIR.mkdir(parents=True, exist_ok=True) + GOLDEN_TESTS_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8") + HISTORY_TEST_SUMMARY_PATH.write_text(json.dumps(summary, indent=2), encoding="utf-8") + return summary + + +def main(): + parser = argparse.ArgumentParser(description="Harvest private Codex chat prompts into local golden tests.") + parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT, help="Maximum tests to add/update this run.") + parser.add_argument("--dry-run", action="store_true", help="Scan and summarize without writing data/golden_tests.json.") + parser.add_argument("--replace-history", action="store_true", help="Remove prior history-harvest tests before adding the new selected batch.") + args = parser.parse_args() + print(json.dumps(harvest(limit=args.limit, dry_run=args.dry_run, replace_history=args.replace_history), indent=2)) + + +if __name__ == "__main__": + main() diff --git a/Codex_CLI_UI/run_golden_batch.py b/Codex_CLI_UI/run_golden_batch.py new file mode 100755 index 0000000..15d5e37 --- /dev/null +++ b/Codex_CLI_UI/run_golden_batch.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +import argparse +import json +import random +import re +import sys +import time +import urllib.request +from datetime import datetime, timezone +from pathlib import Path + + +APP_DIR = Path(__file__).resolve().parent +DATA_DIR = APP_DIR / "data" +RESULT_DIR = DATA_DIR / "golden_batch_results" +DEFAULT_SERVER = "http://127.0.0.1:8765" + +SAFE_SKIP_TERMS = ( + "api key", + "apple id", + "attached", + "authenticator", + "credential", + "delete", + "deploy", + "download", + "ebb", + "github", + "humidity", + "image", + "install", + "ip address", + "iphone", + "log into", + "moonraker", + "nozzle temp", + "password", + "photo", + "pic", + "production", + "qidi", + "restart", + "see photo", + "see photos", + "ssh", + "sudo", + "tailscale", + "token", + "upload", + "vpn", +) + +SAFE_SKIP_PROJECTS = { + "flightops-tracker", + "printer-klipper-ops", + "mac-system-accounts", +} + + +def load_json_url(url, timeout=20): + with urllib.request.urlopen(url, timeout=timeout) as response: + return json.loads(response.read().decode("utf-8")) + + +def post_json_stream(url, payload, timeout=240): + request = urllib.request.Request( + url, + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + for raw in response: + if not raw.strip(): + continue + yield json.loads(raw.decode("utf-8")) + + +def test_messages(test): + messages = test.get("messages") + if isinstance(messages, list) and messages: + return messages + return [{"role": "user", "text": test.get("prompt", "")}] + + +def prompt_text(test): + return "\n".join(str(message.get("text", "")) for message in test_messages(test)).lower() + + +def is_safe_test(test, include_live=False): + if include_live: + return True, "" + if test.get("webSearch") == "live" or test.get("requiresSource"): + return False, "live web/source test" + project = str(test.get("expectedProjectId") or "") + if project in SAFE_SKIP_PROJECTS: + return False, f"skipped project {project}" + text = prompt_text(test) + for term in SAFE_SKIP_TERMS: + if term in text: + return False, f"skip term {term}" + return True, "" + + +def select_tests(tests, args): + if args.ids: + wanted = [item.strip() for item in args.ids.split(",") if item.strip()] + by_id = {item.get("id"): item for item in tests} + selected = [by_id[item_id] for item_id in wanted if item_id in by_id] + missing = [item_id for item_id in wanted if item_id not in by_id] + if missing: + print("Missing test ids: " + ", ".join(missing), file=sys.stderr) + return selected + + candidates = [] + skipped = [] + for test in tests: + if args.group and test.get("group") != args.group: + continue + if args.source and test.get("source") != args.source: + continue + ok, reason = is_safe_test(test, include_live=args.include_live) + if not ok: + skipped.append({"id": test.get("id"), "reason": reason}) + continue + candidates.append(test) + if args.shuffle: + random.Random(args.seed).shuffle(candidates) + if args.offset: + candidates = candidates[args.offset :] + if args.limit: + candidates = candidates[: args.limit] + return candidates + + +def run_test(server, test, args): + payload = { + "profile": test.get("profile") or "manager", + "cwd": args.cwd, + "accessLevel": "danger-full-access", + "reasoningLevel": test.get("reasoningLevel") or "medium", + "managerDepth": test.get("managerDepth") or "fast", + "friendlinessLevel": args.friendliness, + "humorLevel": args.humor, + "webSearch": test.get("webSearch") or "disabled", + "testRun": True, + "messages": test_messages(test), + } + started = time.time() + run = { + "route": {}, + "answer": "", + "analyticalCore": {}, + "taskContract": {}, + "contractGate": {}, + "scorecard": {}, + "returnCode": None, + "thoughts": [], + "warnings": [], + "durationMs": 0, + } + for event in post_json_stream(f"{server}/api/run", payload, timeout=args.timeout): + event_type = event.get("type") + if event_type == "status": + run["route"] = event.get("route") or run["route"] + elif event_type == "assistant": + run["answer"] = event.get("text") or "" + run["analyticalCore"] = event.get("analyticalCore") or {} + run["taskContract"] = event.get("taskContract") or {} + run["contractGate"] = event.get("contractGate") or {} + run["scorecard"] = event.get("scorecard") or {} + elif event_type == "thought": + run["thoughts"].append(event.get("text") or "") + elif event_type in {"warning", "error"}: + run["warnings"].append(event.get("text") or event_type) + elif event_type == "done": + run["returnCode"] = event.get("returnCode") + run["durationMs"] = int((time.time() - started) * 1000) + return run + + +def evaluate_test(test, run): + answer = str(run.get("answer") or "").strip() + lower = answer.lower() + route = run.get("route") or {} + task_contract = run.get("taskContract") or {} + contract_gate = run.get("contractGate") or {} + scorecard = run.get("scorecard") or {} + checks = [] + + def add(label, passed, detail=""): + checks.append({"label": label, "passed": bool(passed), "detail": detail}) + + add( + "answered", + bool(answer) and not re.search(r"no final message returned|returned no answer|run failed", lower), + "final text returned" if answer else "no final text", + ) + if test.get("expectedProjectId"): + add( + "route", + route.get("projectId") == test["expectedProjectId"], + f"expected {test['expectedProjectId']}; got {route.get('projectId') or 'none'}", + ) + if test.get("expectedEngine"): + add( + "engine", + route.get("engine") == test["expectedEngine"], + f"expected {test['expectedEngine']}; got {route.get('engine') or 'none'}", + ) + if test.get("directAnswer"): + first = lower[:280] + terms = test.get("directTerms") or [] + add( + "direct", + any(term.lower() in first for term in terms) if terms else bool(first), + "direct-first language", + ) + if test.get("requiredTerms"): + missing = [term for term in test["requiredTerms"] if term.lower() not in lower] + add("required", not missing, "missing: " + ", ".join(missing) if missing else "required terms found") + if test.get("anyTerms"): + matched = [term for term in test["anyTerms"] if term.lower() in lower] + add("context", bool(matched), "matched: " + ", ".join(matched) if matched else "no anyTerms matched") + if test.get("forbiddenTerms"): + found = [term for term in test["forbiddenTerms"] if term.lower() in lower] + add("grounded", not found, "found: " + ", ".join(found) if found else "no forbidden terms found") + if test.get("requiresSource"): + add("source", "http://" in lower or "https://" in lower or "sources checked" in lower, "source expected") + if test.get("minAnalyticalScore"): + score = float((run.get("analyticalCore") or {}).get("score") or 0) + add("analytical", score >= float(test["minAnalyticalScore"]), f"score {score}") + if test.get("expectedContractKind"): + add( + "contract-kind", + task_contract.get("kind") == test["expectedContractKind"], + f"expected {test['expectedContractKind']}; got {task_contract.get('kind') or 'none'}", + ) + if test.get("expectedContractGate"): + add( + "contract-gate", + contract_gate.get("status") == test["expectedContractGate"], + f"expected {test['expectedContractGate']}; got {contract_gate.get('status') or 'none'}", + ) + if test.get("requiredContractProof"): + proof_text = " ".join(str(item) for item in (task_contract.get("requiredProof") or [])).lower() + missing = [term for term in test["requiredContractProof"] if term.lower() not in proof_text] + add("contract-proof", not missing, "missing: " + ", ".join(missing) if missing else "contract proof terms found") + if test.get("minScorecard"): + score = float(scorecard.get("score") or 0) + add("scorecard", score >= float(test["minScorecard"]), f"score {score}") + return checks + + +def main(): + parser = argparse.ArgumentParser(description="Run Codex CLI UI golden tests through /api/run.") + parser.add_argument("--server", default=DEFAULT_SERVER) + parser.add_argument("--limit", type=int, default=12) + parser.add_argument("--offset", type=int, default=0, help="Skip this many matching safe tests before applying --limit.") + parser.add_argument("--group", default="Slow") + parser.add_argument("--source", default="history-harvest") + parser.add_argument("--ids", default="") + parser.add_argument("--shuffle", action="store_true", help="Shuffle matching safe tests before applying --offset/--limit.") + parser.add_argument("--seed", type=int, default=0, help="Deterministic shuffle seed.") + parser.add_argument("--include-live", action="store_true", help="Allow live web/source and risky project tests.") + parser.add_argument("--cwd", default=str(Path.home() / "Documents/Codex")) + parser.add_argument("--friendliness", default="warm") + parser.add_argument("--humor", default="light") + parser.add_argument("--timeout", type=int, default=240) + parser.add_argument("--no-fail-exit", action="store_true") + args = parser.parse_args() + + bench = load_json_url(f"{args.server}/api/test-bench") + tests = bench.get("tests") or [] + selected = select_tests(tests, args) + started = time.time() + results = [] + + for index, test in enumerate(selected, 1): + run = run_test(args.server, test, args) + checks = evaluate_test(test, run) + status = "pass" if all(check["passed"] for check in checks) else "fail" + record = { + "id": test.get("id"), + "name": test.get("name"), + "group": test.get("group"), + "source": test.get("source"), + "status": status, + "checks": checks, + "durationMs": run.get("durationMs"), + "route": run.get("route"), + "analyticalCore": run.get("analyticalCore"), + "taskContract": run.get("taskContract"), + "contractGate": run.get("contractGate"), + "scorecard": run.get("scorecard"), + "answerPreview": str(run.get("answer") or "")[:1200], + "warnings": run.get("warnings"), + } + results.append(record) + print( + f"{index:02d}/{len(selected):02d} {status.upper()} {record['id']} " + f"{record['durationMs']}ms route={(record['route'] or {}).get('projectId')} " + f"score={(record['analyticalCore'] or {}).get('score')}", + flush=True, + ) + + failed = [item for item in results if item["status"] != "pass"] + summary = { + "runAt": datetime.now(timezone.utc).isoformat(), + "server": args.server, + "selectedCount": len(selected), + "passCount": len(results) - len(failed), + "failCount": len(failed), + "durationMs": int((time.time() - started) * 1000), + "filters": { + "group": args.group, + "source": args.source, + "ids": args.ids, + "includeLive": args.include_live, + "limit": args.limit, + "offset": args.offset, + "shuffle": args.shuffle, + "seed": args.seed, + }, + "results": results, + } + RESULT_DIR.mkdir(parents=True, exist_ok=True) + output_path = RESULT_DIR / (datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-golden-batch.json") + output_path.write_text(json.dumps(summary, indent=2), encoding="utf-8") + print(f"SUMMARY {summary['passCount']} pass {summary['failCount']} fail", flush=True) + print(f"RESULT {output_path}", flush=True) + if failed and not args.no_fail_exit: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/Codex_CLI_UI/server.py b/Codex_CLI_UI/server.py index 51c50f1..0c28a07 100755 --- a/Codex_CLI_UI/server.py +++ b/Codex_CLI_UI/server.py @@ -46,9 +46,12 @@ LOCAL_STRUCTURAL_OUTPUT_DIR = DATA_DIR / "generated" / "structural-fea" LOCAL_DIAGRAM_OUTPUT_DIR = DATA_DIR / "generated" / "engineering-diagrams" LOCAL_QUALITY_OUTPUT_DIR = DATA_DIR / "generated" / "quality-gates" +LOCAL_ORCA_PROFILE_OUTPUT_DIR = DATA_DIR / "generated" / "orca-profiles" SOURCE_VAULT_DIR = DATA_DIR / "source-vault" PRINTING_SOURCE_VAULT_DIR = SOURCE_VAULT_DIR / "3d-printing" PRINTING_SOURCE_INDEX_PATH = PRINTING_SOURCE_VAULT_DIR / "source_index.json" +PUBLIC_TEST_BANK_DIR = APP_DIR / "tests" +MANUFACTURING_TEST_BANK_PATH = PUBLIC_TEST_BANK_DIR / "manufacturing_questions.json" UPLOAD_DIR = DATA_DIR / "uploads" CAPABILITY_TOOL_LOG_PATH = DATA_DIR / "capability_tool_log.jsonl" AUTONOMY_SUPERVISOR_LOG_PATH = DATA_DIR / "autonomy_supervisor.jsonl" @@ -460,6 +463,61 @@ "fiberseeker", "fibreseeker", "push plastics", + "3d printing", + "fdm", + "resin", + "sls", + "warping", + "stringing", + "under-extruding", + "under extruding", + "layer-shifting", + "layer shifting", + "print settings", + "print failed", + ), + "cnc-machining": ( + "cnc", + "machining", + "machine this", + "feeds and speeds", + "feed rate", + "spindle speed", + "milled", + "turned", + "laser cut", + "waterjet", + "chatter", + "surface finish", + "fixture", + "fixturing", + "toolpath", + "pockets", + "contours", + "holes", + "finishing", + ), + "aviation-engineering": ( + "aircraft generate lift", + "climb rate", + "range", + "endurance", + "fuel burn", + "weight and balance", + "stalls", + "spins", + "dutch roll", + "adverse yaw", + "flaps", + "slats", + "spoilers", + "trim", + "control surfaces", + "indicated", + "true airspeed", + "ground speed", + "density altitude", + "performance charts", ), "codex-cli-ui-local-agent": ( "codex cli", @@ -497,6 +555,10 @@ "openfoam", "paraview", "vspaero", + "xfoil", + "openvsp", + "su2", + "qblade", "stress", "strain", "deflection", @@ -514,8 +576,6 @@ "cooling duct", "part cooling", ), - "research-parts-reference": ("fk275", "serpentine belt", "cross reference", "part number"), - "energy-power-research": ("wind turbine", "alternator", "60vdc", "60 vdc", "300 rpm"), "engineering-diagrams": ( "block diagram", "wiring diagram", @@ -528,6 +588,9 @@ "battery backup", "machine architecture", ), + "research-parts-reference": ("fk275", "serpentine belt", "cross reference", "part number"), + "energy-power-research": ("wind turbine", "alternator", "60vdc", "60 vdc", "300 rpm"), + "aviation-engineering": ("aircraft performance", "aerodynamic data", "density altitude", "weight and balance"), "bible-kjv-study": ("king james", "kjv", "bible", "scripture"), } PROJECT_PLAYBOOKS = { @@ -619,7 +682,9 @@ "codex cli", "codex ui", "codex cli ui", "ollama", "local-oss", "local-fast", "startup inventory", "access level", "reasoning", "web access", "dock", "launchagent", "manager agent", "router", - "cloud research", "openai cli", + "cloud research", "openai cli", "test bank", "golden test", + "steer", "edit question", "fix this", "self-healing", "self healing", + "self repair", "package", "github zip", ), "rules": ( "Keep private inventory local unless the user explicitly chooses a cloud path.", @@ -668,6 +733,48 @@ "For mechanical or structural requests, identify loads, constraints, material, process, print orientation, safety factor, and likely failure modes before creating geometry or claiming strength.", ), }, + "cnc-machining": { + "name": "CNC Machining", + "specialist": "CNC Manufacturing Specialist", + "preferred_engine": "local", + "local_profile": "local-oss", + "reasoning": "high", + "triggers": ( + "cnc", "machining", "machine this", "feeds and speeds", "feed rate", + "spindle speed", "sfm", "chip load", "milled", "turned", "lathe", + "mill", "laser cut", "waterjet", "chatter", "surface finish", + "fixture", "fixturing", "toolpath", "pockets", "contours", + "holes", "finishing", "machining cost", + ), + "rules": ( + "Start by identifying material, machine rigidity, cutter/tooling, operation, workholding, and tolerance requirements.", + "For feeds and speeds, state assumptions and formulas instead of inventing a universal RPM/feed.", + "For chatter or finish problems, rank causes: rigidity/workholding, tool stickout, cutter geometry, chip load, spindle speed, depth/width of cut, coolant, and toolpath.", + "For make/buy/process choices, compare machining, turning, laser/waterjet, and additive manufacturing by tolerance, material, geometry, cost, and setup risk.", + ), + }, + "aviation-engineering": { + "name": "Aviation Engineering", + "specialist": "Aviation Engineering Specialist", + "preferred_engine": "local", + "local_profile": "local-oss", + "reasoning": "high", + "triggers": ( + "aircraft generate lift", "aircraft performance", "climb rate", + "range", "endurance", "fuel burn", "weight and balance", + "stall", "stalls", "spin", "spins", "dutch roll", "adverse yaw", + "flaps", "slats", "spoilers", "trim", "control surface", + "control surfaces", "indicated airspeed", "true airspeed", + "ground speed", "density altitude", "performance chart", + "performance charts", "aerodynamic data", + ), + "rules": ( + "Separate conceptual aviation education from flight-planning or operational advice.", + "For performance questions, name the controlling variables and explain directionally before doing math.", + "For weight and balance, keep units, arm, moment, datum, and envelope checks explicit.", + "For stalls/spins/control-surface questions, explain the aerodynamic mechanism and the practical consequence without pretending to replace aircraft-specific manuals or training.", + ), + }, "research-parts-reference": { "name": "Research, Parts & Cross-Reference", "specialist": "Parts Research Specialist", @@ -684,23 +791,6 @@ "Show why near-matches fail so Tinman can avoid buying the wrong part.", ), }, - "energy-power-research": { - "name": "Energy & Power Research", - "specialist": "Energy Research Specialist", - "preferred_engine": "local-research", - "local_profile": "local-oss", - "reasoning": "high", - "triggers": ( - "wind turbine", "alternator", "generator", "60vdc", "60 vdc", - "3 phase", "rectified", "300 rpm", "battery", "solar", - "charge controller", "inverter", - ), - "rules": ( - "For electrical recommendations, verify voltage, RPM, phase/output type, power rating, and price separately.", - "Do not extrapolate voltage at RPM unless you mark it as an estimate and explain load sag.", - "Lead with one best practical pick, then list rejects or seller-confirmation questions.", - ), - }, "engineering-diagrams": { "name": "Engineering Diagrams", "specialist": "Systems Diagram Engineer", @@ -709,8 +799,7 @@ "reasoning": "high", "triggers": ( "block diagram", "wiring diagram", "electrical diagram", "schematic", - "architecture diagram", "system diagram", "power diagram", "solar", - "backup battery", "battery backup", "power grid", "grid tie", "inverter", + "architecture diagram", "system diagram", "power diagram", "grid tie", "drawio", "draw.io", "graphviz", "dot file", "mermaid", "kicad", "connector", "pinout", "wire gauge", "wiring harness", "cnc", "3d printer architecture", ), @@ -721,6 +810,23 @@ "Use Graphviz for clean layout when available, draw.io XML for editing, and KiCad notes/files when connector-level schematic work is needed.", ), }, + "energy-power-research": { + "name": "Energy & Power Research", + "specialist": "Energy Research Specialist", + "preferred_engine": "local-research", + "local_profile": "local-oss", + "reasoning": "high", + "triggers": ( + "wind turbine", "alternator", "generator", "60vdc", "60 vdc", + "3 phase", "rectified", "300 rpm", "battery", "solar", + "charge controller", "inverter", + ), + "rules": ( + "For electrical recommendations, verify voltage, RPM, phase/output type, power rating, and price separately.", + "Do not extrapolate voltage at RPM unless you mark it as an estimate and explain load sag.", + "Lead with one best practical pick, then list rejects or seller-confirmation questions.", + ), + }, "bible-kjv-study": { "name": "Bible & KJV Study", "specialist": "KJV Study Specialist", @@ -846,6 +952,9 @@ "directAnswer": True, "directTerms": ["19 mm", "about 19 mm", "15 mm"], "requiredTerms": ["this is why", "you should also consider"], + "expectedContractKind": "Direct answer", + "expectedContractGate": "pass", + "requiredContractProof": ["direct answer", "why/caveat"], "forbiddenTerms": ["fusion 360 script", "openscad model", "staged", "cad package"], "minAnalyticalScore": 82, "goal": "Answer the hose-size question directly instead of staging CAD artifacts.", @@ -862,6 +971,9 @@ "directAnswer": True, "directTerms": [".f3d", ".f3z", ".step"], "requiredTerms": [".f3d", ".f3z", ".step", "stl"], + "expectedContractKind": "CAD reference", + "expectedContractGate": "pass", + "requiredContractProof": ["direct format recommendation", "why/caveat"], "forbiddenTerms": ["fusion 360 script:", "openscad model:", "staged a first-pass"], "minAnalyticalScore": 82, "goal": "Answer the CAD reference question without creating unrelated artifacts.", @@ -914,6 +1026,38 @@ "minAnalyticalScore": 82, "goal": "Pull the actual local Orca/TinmanX1 PET-CF 0.6 nozzle filament profile instead of returning machine specs or generic tuning.", }, + { + "id": "hard-orca-profile-creation-not-pull", + "name": "Orca Profile Creation Not Pull", + "group": "Hard Cases", + "prompt": "Can you create an Orca PETG filament profile for all printers all nozzle sizes? Use the best practices and lessons learned from what we have done and industry. Use all the resources you have available for a perfect profile.", + "profile": "manager", + "managerDepth": "fast", + "webSearch": "disabled", + "expectedProjectId": "tinmanx-slicer-research", + "directAnswer": True, + "directTerms": ["created", "starter profile pack", "orca petg"], + "requiredTerms": ["petg", "profile pack", "calibration", "this is why", "you should also consider"], + "forbiddenTerms": ["actual local slicer profile data", "petg-cf", "machine specs", "fusion 360"], + "minAnalyticalScore": 82, + "goal": "Create a starter Orca filament-profile package for the requested material instead of pulling an unrelated existing profile.", + }, + { + "id": "hard-orca-nozzle-visibility-options", + "name": "Orca Nozzle Visibility Options", + "group": "Hard Cases", + "prompt": "I am able to sync the filament type and color into the prepare tab. I would still like to be able to see what nozzle is installed on the machine and what type in Orca. What are my options at this point?", + "profile": "manager", + "managerDepth": "fast", + "webSearch": "disabled", + "expectedProjectId": "tinmanx-slicer-research", + "directAnswer": True, + "directTerms": ["configured nozzle", "installed physical nozzle", "local inventory"], + "requiredTerms": ["orca", "this is why", "you should also consider"], + "forbiddenTerms": ["ssh password", "password", "fusion 360", "cad package", "moonraker status only"], + "minAnalyticalScore": 82, + "goal": "Treat Orca nozzle display/sync questions as slicer-profile workflow design, not live printer status or CAD.", + }, { "id": "hard-pctg-temp-tower-image", "name": "PCTG Temp Tower Image", @@ -979,6 +1123,8 @@ "expectedProjectId": "cad-modeling-projects", "expectedEngine": "local", "requiredTerms": ["fusion 360", "18", "12-15 cfm", "cfd", "validation"], + "expectedContractKind": "CAD/design deliverable", + "requiredContractProof": ["clickable CAD/script/readme files", "assumptions", "fit/validation"], "anyTerms": ["duct", "plenum", "outlet", "airflow", "artifact"], "forbiddenTerms": ["moonraker", "unreachable", "printer status", "qidi plus 4", "nozzle temperature"], "minAnalyticalScore": 82, @@ -1011,6 +1157,9 @@ "expectedProjectId": "cad-modeling-projects", "directAnswer": True, "requiredTerms": ["did not find a readable stl", "attach the stl", "stopped before generating fake duct geometry"], + "expectedContractKind": "STL/CAD deliverable", + "expectedContractGate": "pass", + "requiredContractProof": ["source STL status", "missing-attachment blocker", "validation limits"], "forbiddenTerms": ["i generated an inferred", "duct stl:", "airway stl", "fusion 360 script", "openscad model"], "minAnalyticalScore": 82, "goal": "If macOS pasted only an STL filename, stop before inventing geometry and ask for the actual file.", @@ -1022,6 +1171,8 @@ "hard-marlin-diagnostic", "hard-orca-filament-profile", "hard-orca-current-petcf-06-profile", + "hard-orca-profile-creation-not-pull", + "hard-orca-nozzle-visibility-options", "hard-pctg-temp-tower-image", "hard-pctg-temp-tower-pa-followup", "hard-cpap-duct-design-not-status", @@ -1029,6 +1180,394 @@ "hard-stl-filename-missing-attachment", } +CONTRACT_GATE_GOLDEN_TEST_IDS = { + "hard-cpap-hose-id", + "hard-fusion-component-format", + "hard-cpap-duct-design-not-status", + "hard-stl-filename-missing-attachment", +} + + +DOMAIN_SAMPLE_QUESTION_GROUPS = { + "3D Printing": { + "project": "tinmanx-slicer-research", + "questions": ( + "What material should I use for this part: PLA, PETG, ABS, ASA, nylon, carbon-fiber filled, etc.?", + "Why is my print warping, stringing, under-extruding, or layer-shifting?", + "What print settings should I use for strength, heat resistance, or surface finish?", + "How should I orient this part for best strength?", + "Can this part be redesigned to print without supports?", + "What tolerances should I design for FDM, resin, or SLS printing?", + "Why did this print fail halfway through?", + "Is this part strong enough for its intended use?", + ), + }, + "CNC Machining": { + "project": "cnc-machining", + "questions": ( + "What material should I machine this from?", + "What feeds and speeds should I use?", + "Should this part be milled, turned, laser cut, waterjet cut, or 3D printed?", + "How do I reduce chatter or poor surface finish?", + "What tolerances are realistic for this geometry?", + "How should I fixture this part?", + "Can this design be simplified to reduce machining cost?", + "What toolpath strategy should I use for pockets, contours, holes, or finishing?", + ), + }, + "Solar And Wind Technology": { + "project": "energy-power-research", + "questions": ( + "How large of a solar system do I need for my house, cabin, RV, or equipment?", + "How many panels and batteries are required for a given load?", + "What size charge controller or inverter do I need?", + "Is wind power practical at my location?", + "How do I compare solar versus wind for off-grid power?", + "What affects solar panel efficiency?", + "How much power can I realistically generate per day?", + "How do battery chemistry, depth of discharge, and temperature affect system design?", + ), + }, + "Aerodynamics": { + "project": "cad-modeling-projects", + "questions": ( + "How does airfoil shape affect lift and drag?", + "What causes stall, separation, turbulence, or vortex formation?", + "How do I reduce drag on a vehicle, aircraft, duct, or enclosure?", + "What is the difference between lift coefficient, drag coefficient, and Reynolds number?", + "How does angle of attack affect performance?", + "What wing shape or control surface layout should I use?", + "How do propellers, fans, and ducts behave aerodynamically?", + "How do I estimate aerodynamic forces without full simulation?", + ), + }, + "CFD Analysis": { + "project": "cad-modeling-projects", + "questions": ( + "What CFD setup should I use for this problem?", + "What boundary conditions are appropriate?", + "How fine does the mesh need to be?", + "Which turbulence model should I use?", + "Why is my CFD solution not converging?", + "How do I interpret pressure, velocity, vorticity, and streamline plots?", + "Is my CFD result physically realistic?", + "How do I validate CFD results against hand calculations or test data?", + ), + }, + "Engineering": { + "project": "cad-modeling-projects", + "questions": ( + "Is this design strong enough?", + "What material, thickness, fastener size, or weld type should I use?", + "How do I calculate load, stress, torque, pressure, or deflection?", + "What factor of safety is appropriate?", + "How can this part be redesigned to be cheaper, stronger, lighter, or easier to manufacture?", + "What failure modes should I worry about?", + "How do I turn an idea into a manufacturable design?", + "Can you review this sketch, CAD concept, or drawing for problems?", + ), + }, + "Aviation": { + "project": "aviation-engineering", + "questions": ( + "How do aircraft generate lift?", + "What affects climb rate, range, endurance, and fuel burn?", + "How do weight and balance calculations work?", + "What causes stalls, spins, Dutch roll, or adverse yaw?", + "How do flaps, slats, spoilers, trim, and control surfaces work?", + "What is the difference between indicated, true, and ground speed?", + "How do weather, density altitude, and wind affect performance?", + "How do I interpret aircraft performance charts or aerodynamic data?", + ), + }, +} + + +FUSION_ORCA_SAMPLE_QUESTION_GROUPS = { + "Fusion 360": { + "project": "cad-modeling-projects", + "questions": ( + "How do I export a STEP file while keeping component and body names useful?", + "How should I set up user parameters for a printed bracket so I can resize it later?", + "Why did my Fusion sketch turn under-constrained, and how do I fix it cleanly?", + "When should I use joints instead of align or move/copy in an assembly?", + "How much clearance should I model between a printed peg and hole for FDM?", + "How do I turn an STL mesh into an editable solid without destroying the geometry?", + "How should I set the origin and axes before exporting a part for CNC or 3D printing?", + "How do I use construction planes to make accurate angled features?", + "Why does my loft or sweep twist, fail, or create ugly geometry?", + "How do I prepare a modeled part for CAM toolpaths in the Manufacture workspace?", + "What are good Fusion 360 practices for fillets and chamfers on 3D printed parts?", + "How do I make a drawing with useful dimensions and tolerances from a Fusion model?", + "How should I split a body so a large part prints cleanly without support?", + "How should I model heat-set inserts, screw bosses, and threaded holes?", + "How do I export STL or 3MF with the right units, orientation, and resolution?", + "What information do you need from me before making a parametric Fusion 360 script?", + "How do I diagnose a timeline feature that broke after I changed an early sketch?", + "How should I organize components, bodies, sketches, and construction geometry in a real assembly?", + "How do I design snap fits or tabs that will survive repeated use?", + "How do I check whether a Fusion model is manufacturable before I print or machine it?", + ), + }, + "OrcaSlicer Codex App": { + "project": "orcaslicer-codex", + "questions": ( + "How do I fix a missing custom preset after updating Orca Slicer?", + "How do I copy a preset safely without overwriting the system profile?", + "How do I troubleshoot a printer host mapping that does not persist after restart?", + "How do I back up Orca user presets before editing them?", + "How do I compare two Orca profiles and find the setting that changed?", + "How do I verify the installed app sees a new preset after I add it?", + "How do I safely edit OrcaSlicer.conf without the app overwriting it?", + "How do I diagnose an Orca printer host mapping that keeps reconnecting or disappearing?", + ), + }, + "Orca Profile Workflow": { + "project": "tinmanx-slicer-research", + "questions": ( + "How do I tell whether Orca is using the machine profile, filament profile, or process profile for a setting?", + "Why does Orca show my filament but not the printer or nozzle I expect?", + "What do machine, filament, and process profiles each control when tuning a filament in Orca?", + "How do I keep custom printer, filament, and process profiles organized for Orca calibration across machines?", + "What should I check when the prepare tab and device tab disagree about printer or filament state?", + ), + }, + "Orca Filament Calibration": { + "project": "tinmanx-slicer-research", + "questions": ( + "What order should I run Orca filament calibration tests for a brand-new material?", + "How do I tune filament flow ratio in Orca without hiding an extrusion problem?", + "How do I read a filament temperature tower and choose the best nozzle temperature?", + "How do I tune filament pressure advance or K value from an Orca calibration print?", + "How do I create a PET-CF filament profile for a 0.6 nozzle on my Qidi Plus 4?", + "What filament settings matter most for ASA warping and corner lift in Orca?", + "How should I set filament max volumetric speed for a new high-flow material?", + "How do I decide filament part cooling for PLA, ABS, ASA, PCTG, and PET-CF?", + "How should I set filament retraction for a direct-drive printer in Orca?", + "How do I copy a filament profile across machines, nozzle sizes, and material variants?", + "How do I tune PET-CF without accidentally using PETG-CF assumptions?", + "How do I use shrinkage compensation or XY compensation for dimensionally accurate parts?", + "How should I tune supports for easy removal without ruining overhang quality?", + "How should I set filament bed, nozzle, and chamber temperatures for nylon or polycarbonate?", + "How should I save Orca calibration results per filament profile, material, nozzle size, and speed range?", + "How do I diagnose stringing if temperature, retraction, and filament dryness all interact?", + "How do I decide whether a bad print is caused by filament settings, process settings, or machine limits?", + ), + }, +} + + +def domain_sample_test_id(category, index): + clean = re.sub(r"[^a-z0-9]+", "-", category.lower()).strip("-") + return f"domain-sample-{clean}-{index:02d}" + + +def fusion_orca_sample_test_id(category, index): + clean = re.sub(r"[^a-z0-9]+", "-", category.lower()).strip("-") + return f"fusion-orca-sample-{clean}-{index:02d}" + + +def domain_sample_golden_tests(): + tests = [] + for category, info in DOMAIN_SAMPLE_QUESTION_GROUPS.items(): + project_id = info["project"] + for index, question in enumerate(info["questions"], 1): + prompt = f"{category}: {question}" + tests.append( + { + "id": domain_sample_test_id(category, index), + "name": prompt if len(prompt) <= 58 else prompt[:46].rstrip() + " [truncated]", + "group": "Domain Samples", + "prompt": prompt, + "profile": "manager", + "managerDepth": "fast", + "webSearch": "disabled", + "expectedProjectId": project_id, + "directAnswer": True, + "directTerms": [], + "requiredTerms": ["this is why", "you should also consider"], + "forbiddenTerms": [ + "run failed", + "no final message returned", + "no response", + "load failed", + "recovery plan:", + "i do not have access", + "i don't have access", + "you can check it yourself", + ], + "minAnalyticalScore": 74, + "goal": "Curated real-world domain sample: route to the right expert lane and answer in Tinman's direct why/consider style.", + "source": "domain-sample", + } + ) + return tests + + +def fusion_orca_sample_golden_tests(): + tests = [] + for category, info in FUSION_ORCA_SAMPLE_QUESTION_GROUPS.items(): + project_id = info["project"] + for index, question in enumerate(info["questions"], 1): + prompt = f"{category}: {question}" + tests.append( + { + "id": fusion_orca_sample_test_id(category, index), + "name": prompt if len(prompt) <= 58 else prompt[:46].rstrip() + " [truncated]", + "group": "Fusion & Orca Samples", + "prompt": prompt, + "profile": "manager", + "managerDepth": "fast", + "webSearch": "disabled", + "expectedProjectId": project_id, + "directAnswer": True, + "directTerms": [], + "requiredTerms": ["this is why", "you should also consider"], + "forbiddenTerms": [ + "run failed", + "no final message returned", + "no response", + "load failed", + "recovery plan:", + "printer status", + "fusion 360 script:", + "openscad model:", + "staged a first-pass cad package", + "machine specs only", + "i do not have access", + "i don't have access", + "you can check it yourself", + ], + "minAnalyticalScore": 72, + "goal": "Common Fusion 360 and Orca Slicer sample: route correctly, answer directly, and avoid wrong-tool or file-receipt detours.", + "source": "fusion-orca-sample", + } + ) + return tests + + +TINMANX1_POLYMAKER_SCENARIO_PROMPT = ( + "Lets move to TinmanX1. In TinmanX1 I would like you to update the Polymaker filament system preset " + "to include all of the current filaments available by polymaker to include the Fiberon line. the print " + "settings should be available on the website. I also have attached 100 more questions to test. I would " + "also like a steer and edit question functions just like yours wired in in the same location that you have. " + "another addition, when I tell him he is wrong, I want him to figure out why and fix his own code to make " + "sure it doesnt happen again. Once done with that, lets update github. I would like the entire package that " + "we created including all of the downloads available in 1 github zip file so that my friends can download " + "exactly what we have created" +) + + +def load_manufacturing_question_bank(): + try: + data = json.loads(MANUFACTURING_TEST_BANK_PATH.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return [] + rows = data.get("questions") if isinstance(data, dict) else [] + if not isinstance(rows, list): + return [] + normalized = [] + for index, row in enumerate(rows, 1): + if not isinstance(row, dict): + continue + question = str(row.get("question") or "").strip() + category = str(row.get("category") or "").strip() + if not question or category not in {"CAD", "CNC Machining"}: + continue + try: + question_id = int(row.get("id") or index) + except (TypeError, ValueError): + question_id = index + normalized.append({"id": question_id, "category": category, "question": question}) + return normalized + + +def manufacturing_sample_test_id(row): + category = "cad" if row.get("category") == "CAD" else "cnc" + return f"manufacturing-sample-{category}-{int(row.get('id') or 0):03d}" + + +def manufacturing_sample_golden_tests(): + tests = [] + for row in load_manufacturing_question_bank(): + category = row["category"] + prompt = f"{category}: {row['question']}" + expected_project = "cad-modeling-projects" if category == "CAD" else "cnc-machining" + tests.append( + { + "id": manufacturing_sample_test_id(row), + "name": prompt if len(prompt) <= 58 else prompt[:46].rstrip() + " [truncated]", + "group": "Manufacturing Samples", + "prompt": prompt, + "profile": "manager", + "managerDepth": "fast", + "webSearch": "disabled", + "expectedProjectId": expected_project, + "directAnswer": True, + "directTerms": [], + "requiredTerms": ["this is why", "you should also consider"], + "forbiddenTerms": [ + "run failed", + "no final message returned", + "no response", + "load failed", + "recovery plan:", + "fusion 360 script:", + "openscad model:", + "staged a first-pass cad package", + "i do not have access", + "i don't have access", + "you can check it yourself", + ], + "minAnalyticalScore": 76, + "goal": "Public manufacturing sample: route CAD and CNC questions correctly and answer directly in Tinman's why/consider style.", + "source": "manufacturing-sample", + } + ) + return tests + + +SCENARIO_GOLDEN_TESTS = [ + { + "id": "scenario-tinmanx1-polymaker-steer-self-repair-release", + "name": "TinmanX1 Polymaker UI Self-Repair Release Scenario", + "group": "Workflow Scenarios", + "prompt": TINMANX1_POLYMAKER_SCENARIO_PROMPT, + "profile": "manager", + "managerDepth": "fast", + "webSearch": "disabled", + "expectedProjectId": "codex-cli-ui-local-agent", + "directAnswer": True, + "requiredTerms": [ + "polymaker", + "fiberon", + "steer", + "edit", + "self-healing", + "github", + "zip", + "this is why", + "you should also consider", + ], + "forbiddenTerms": [ + "no final message returned", + "load failed", + "recovery plan:", + "i cannot", + "i can't", + ], + "minAnalyticalScore": 82, + "goal": "Complex Codex CLI UI/TinmanX1 request should become a sequenced implementation plan with test-bank, UI, self-repair, release, and packaging cautions.", + "source": "workflow-scenario", + } +] + + +GOLDEN_TESTS.extend(domain_sample_golden_tests()) +GOLDEN_TESTS.extend(fusion_orca_sample_golden_tests()) +GOLDEN_TESTS.extend(manufacturing_sample_golden_tests()) +GOLDEN_TESTS.extend(SCENARIO_GOLDEN_TESTS) + def default_generated_golden_tests(): now = time.time() @@ -1285,6 +1824,108 @@ def hard_case_golden_tests_synthetic_check(): return True +def contract_gate_golden_tests_synthetic_check(): + by_id = {test.get("id"): test for test in golden_tests()} + if not CONTRACT_GATE_GOLDEN_TEST_IDS.issubset(by_id): + return False + for test_id in CONTRACT_GATE_GOLDEN_TEST_IDS: + test = by_id[test_id] + if not test.get("expectedContractKind"): + return False + if not test.get("requiredContractProof"): + return False + return True + + +def domain_sample_golden_tests_synthetic_check(): + expected_count = sum(len(info["questions"]) for info in DOMAIN_SAMPLE_QUESTION_GROUPS.values()) + tests = [test for test in golden_tests() if test.get("source") == "domain-sample"] + if len(tests) != expected_count: + return False + for test in tests: + route = route_manager( + [{"role": "user", "text": test.get("prompt", "")}], + requested_profile="manager", + web_search="disabled", + ) + if route.get("projectId") != test.get("expectedProjectId"): + return False + if test.get("group") != "Domain Samples": + return False + if "this is why" not in test.get("requiredTerms", []): + return False + return True + + +def fusion_orca_sample_golden_tests_synthetic_check(): + expected_count = sum(len(info["questions"]) for info in FUSION_ORCA_SAMPLE_QUESTION_GROUPS.values()) + tests = [test for test in golden_tests() if test.get("source") == "fusion-orca-sample"] + if len(tests) != expected_count: + return False + seen = {category: 0 for category in FUSION_ORCA_SAMPLE_QUESTION_GROUPS} + for test in tests: + prompt = str(test.get("prompt") or "") + category = prompt.split(":", 1)[0] + info = FUSION_ORCA_SAMPLE_QUESTION_GROUPS.get(category) + if not info: + return False + seen[category] += 1 + route = route_manager( + [{"role": "user", "text": prompt}], + requested_profile="manager", + web_search="disabled", + ) + if route.get("projectId") != info.get("project"): + return False + if test.get("group") != "Fusion & Orca Samples": + return False + if "this is why" not in test.get("requiredTerms", []): + return False + if "you should also consider" not in test.get("requiredTerms", []): + return False + return all(seen[category] == len(info["questions"]) for category, info in FUSION_ORCA_SAMPLE_QUESTION_GROUPS.items()) + + +def manufacturing_sample_golden_tests_synthetic_check(): + rows = load_manufacturing_question_bank() + tests = [test for test in golden_tests() if test.get("source") == "manufacturing-sample"] + if len(rows) < 100 or len(tests) != len(rows): + return False + expected = { + "CAD": "cad-modeling-projects", + "CNC Machining": "cnc-machining", + } + seen = {"CAD": 0, "CNC Machining": 0} + for row in rows: + seen[row["category"]] += 1 + prompt = f"{row['category']}: {row['question']}" + route = route_manager( + [{"role": "user", "text": prompt}], + requested_profile="manager", + web_search="disabled", + ) + if route.get("projectId") != expected[row["category"]]: + return False + return seen["CAD"] == 50 and seen["CNC Machining"] == 50 + + +def workflow_scenario_golden_tests_synthetic_check(): + by_id = {test.get("id"): test for test in golden_tests()} + test = by_id.get("scenario-tinmanx1-polymaker-steer-self-repair-release") + if not test or test.get("expectedProjectId") != "codex-cli-ui-local-agent": + return False + route = route_manager( + [{"role": "user", "text": test.get("prompt", "")}], + requested_profile="manager", + web_search="disabled", + ) + return ( + route.get("projectId") == "codex-cli-ui-local-agent" + and "self-healing" in test.get("requiredTerms", []) + and test.get("source") == "workflow-scenario" + ) + + BENCHMARK_TESTS = [ { "id": "fast-direct", @@ -2495,7 +3136,7 @@ def is_printer_machine(machine): name = str(machine.get("name", "")).lower() notes = str(machine.get("notes", "")).lower() text = f"{name} {notes}" - if any(term in name for term in ("router", "vpn gateway", "netgear")): + if any(term in name for term in ("router", "vpn", "netgear")): return False printer_terms = ( "qidi", @@ -4269,7 +4910,6 @@ def is_cad_reference_question(messages): "body", "assembly", "mesh", - "slicer", ) reference_terms = ( "file type", @@ -4342,6 +4982,26 @@ def route_query_text(messages, cwd=""): return "\n".join(recent).lower() +def domain_sample_project_override(text): + text = str(text or "").lower().strip() + prefix_map = ( + ("3d printing:", "tinmanx-slicer-research"), + ("3d-printing:", "tinmanx-slicer-research"), + ("cad:", "cad-modeling-projects"), + ("cnc machining:", "cnc-machining"), + ("solar and wind technology:", "energy-power-research"), + ("solar/wind technology:", "energy-power-research"), + ("aerodynamics:", "cad-modeling-projects"), + ("cfd analysis:", "cad-modeling-projects"), + ("engineering:", "cad-modeling-projects"), + ("aviation:", "aviation-engineering"), + ) + for prefix, project_id in prefix_map: + if text.startswith(prefix): + return project_id + return "" + + def project_from_thread(messages): for message in reversed(messages or []): route = message.get("route") if isinstance(message, dict) else None @@ -4398,20 +5058,43 @@ def is_engineering_diagram_request(messages): return False +def is_codex_ui_workflow_scenario_request(messages): + query = latest_user_text(messages).lower() + if not query: + return False + ui_terms = ("steer", "edit question", "fix this", "self-healing", "self healing", "github", "zip", "test bank") + return ( + text_has_any(query, ("codex cli ui", "codex ui", "your son", "him", "tinmanx1")) + and text_has_any(query, ("steer", "edit question")) + and text_has_any(query, ("self-healing", "self healing", "fix his own code", "figure out why")) + and text_has_any(query, ("github", "zip", "package")) + and sum(1 for term in ui_terms if term in query) >= 4 + ) + + def route_manager(messages, cwd="", requested_profile=DEFAULT_PROFILE, web_search="live"): text = route_query_text(messages, cwd) + domain_override = domain_sample_project_override(text) previous_project = project_from_thread(messages) cpap_hose_spec = is_cpap_hose_spec_question(messages) cooling_duct_research = is_cooling_duct_research_request(messages) cad_reference = is_cad_reference_question(messages) cad_design = is_cad_design_request(messages) + fusion_cam = is_fusion_cam_question(messages) stl_cfd_design = is_stl_cfd_duct_design_request(messages) engineering_diagram = is_engineering_diagram_request(messages) + codex_ui_workflow_scenario = is_codex_ui_workflow_scenario_request(messages) + orca_profile_creation = is_orca_profile_creation_request(messages) + orca_nozzle_visibility = is_orca_nozzle_visibility_question(messages) + orca_slicer_workflow = is_orca_slicer_workflow_question(messages) printing_calibration_or_profile = ( is_filament_profile_pull_request(messages) + or orca_profile_creation + or orca_slicer_workflow or is_temperature_tower_image_question(messages) or is_temperature_tower_pressure_advance_followup(messages) or is_orca_calibration_image_question(messages) + or orca_nozzle_visibility ) public_printer_research = wants_public_printer_research(messages) and not (cad_design or stl_cfd_design) scores = [] @@ -4447,7 +5130,19 @@ def route_manager(messages, cwd="", requested_profile=DEFAULT_PROFILE, web_searc else: score, project_id, matched = 0, "general", [] - if engineering_diagram: + if domain_override: + project_id = domain_override + score = max(score, 40) + matched = ["domain-sample"] + [item for item in matched if item != "domain-sample"] + elif codex_ui_workflow_scenario: + project_id = "codex-cli-ui-local-agent" + score = max(score, 42) + matched = ["codex-ui-workflow-scenario"] + [item for item in matched if item != "codex-ui-workflow-scenario"] + elif fusion_cam: + project_id = "cad-modeling-projects" + score = max(score, 34) + matched = ["fusion-cam"] + [item for item in matched if item != "fusion-cam"] + elif engineering_diagram: project_id = "engineering-diagrams" score = max(score, 36) matched = ["engineering-diagram"] + [item for item in matched if item != "engineering-diagram"] @@ -4471,6 +5166,18 @@ def route_manager(messages, cwd="", requested_profile=DEFAULT_PROFILE, web_searc project_id = "cad-modeling-projects" score = max(score, 32) matched = ["cad-design"] + [item for item in matched if item != "cad-design"] + elif orca_profile_creation: + project_id = "tinmanx-slicer-research" + score = max(score, 34) + matched = ["orca-profile-creation"] + [item for item in matched if item != "orca-profile-creation"] + elif orca_nozzle_visibility: + project_id = "tinmanx-slicer-research" + score = max(score, 34) + matched = ["orca-nozzle-visibility"] + [item for item in matched if item != "orca-nozzle-visibility"] + elif orca_slicer_workflow: + project_id = "tinmanx-slicer-research" + score = max(score, 34) + matched = ["orca-slicer-workflow"] + [item for item in matched if item != "orca-slicer-workflow"] elif printing_calibration_or_profile: project_id = "tinmanx-slicer-research" score = max(score, 32) @@ -5022,6 +5729,7 @@ def build_analytical_context(messages, route=None, web_search="live", local_tool return "" profile = detected_domain_profile(messages, route or {}) core = analytical_core_profile(messages, route or {}, web_search=web_search, local_tools=local_tools) + contract = task_contract(messages, route or {}) decision = core.get("decisionFrame") or {} lines = [ "Analytical operating system:", @@ -5044,7 +5752,18 @@ def build_analytical_context(messages, route=None, web_search="live", local_tool f"- Analytical mode: {core.get('mode')} ({core.get('complexity')} complexity, {core.get('riskLevel')} risk).", f"- Actual objective: {core.get('objective')}.", f"- Done means: {core.get('doneMeans')}.", + "", + "Task contract gate:", + f"- Task type: {contract.get('kind')}.", + f"- Done means: {contract.get('doneMeans')}.", ] + if contract.get("mustDo"): + lines.append(f"- Must do before final: {', '.join(contract.get('mustDo')[:8])}.") + if contract.get("requiredProof"): + lines.append(f"- Required proof: {', '.join(contract.get('requiredProof')[:8])}.") + if contract.get("rejectIf"): + lines.append(f"- Reject the answer if: {', '.join(contract.get('rejectIf')[:8])}.") + lines.append("") if core.get("explicitConstraints"): lines.append(f"- Explicit constraints to preserve: {', '.join(core.get('explicitConstraints')[:8])}.") if decision.get("criteria"): @@ -5074,8 +5793,25 @@ def build_analytical_context(messages, route=None, web_search="live", local_tool def analytical_core_mode(messages, route=None): query = latest_user_text(messages).lower() route_id = (route or {}).get("projectId", "") + if "ai" in query and "print" in query and "failure" in query and text_has_any(query, ("monitor", "detection", "detect")): + return "decision" + if text_has_any(query, ("tinmanx", "orca", "rocket slicer", "bambu slicer")) and text_has_any(query, ("facelift", "new look", "colors", "opening tile", "branding", "theme")): + return "implementation" + if is_orca_profile_creation_request(messages): + return "implementation" + if is_fusion_cam_question(messages): + return "diagnostics" + if is_orca_slicer_workflow_question(messages): + query_for_mode = latest_user_text(messages).lower() + if text_has_any(query_for_mode, ("facelift", "new look", "colors", "opening tile", "branding", "theme")): + return "implementation" + if text_has_any(query_for_mode, ("is there any way", "options", "more control", "device tab", "devive tab")): + return "decision" + return "diagnostics" if is_cpap_hose_spec_question(messages) or is_fusion_component_export_question(messages) or is_filament_profile_pull_request(messages): return "direct-answer" + if is_orca_nozzle_visibility_question(messages): + return "decision" if is_temperature_tower_image_question(messages) or is_temperature_tower_pressure_advance_followup(messages) or is_orca_calibration_image_question(messages): return "decision" if is_engineering_diagram_request(messages): @@ -5088,6 +5824,8 @@ def analytical_core_mode(messages, route=None): return "evidence-research" if text_has_any(query, ("compare", "best", "choose", "recommend", "which", "should i", "price", "availability")): return "decision" + if text_has_any(query, ("what action should", "how do i stop", "what should i do")): + return "decision" if text_has_any(query, ("write", "code", "script", "macro", "config", "fix", "implement", "build", "app", "github", "repo")): return "implementation" if text_has_any(query, ("calculate", "formula", "size", "dimension", "load", "flow", "current", "voltage", "rpm")): @@ -5118,6 +5856,12 @@ def analytical_core_complexity(messages): def analytical_core_risk(messages, route=None): query = latest_user_text(messages).lower() + if is_orca_profile_creation_request(messages): + return "normal" + if is_fusion_cam_question(messages) or is_orca_slicer_workflow_question(messages): + return "normal" + if is_orca_nozzle_visibility_question(messages): + return "normal" if text_has_any(query, ("live printer", "restart", "upload", "heater", "nozzle", "bed", "moonraker", "ssh", "password", "credential", "delete", "reset", "sudo")): return "live-system" if text_has_any(query, ("wire", "wiring", "electrical", "solar", "battery", "grid", "vfd", "mains", "240", "120", "vac", "breaker", "fuse", "e-stop", "estop")): @@ -5339,8 +6083,10 @@ def build_direct_answer_context(messages, route): "what is", "what are", "what is the best", + "what information", "what file", "what format", + "what do", "best all around", "best first step", "can you tell me", @@ -5348,6 +6094,15 @@ def build_direct_answer_context(messages, route): "how much", "how many", "which", + "how do i", + "how should i", + "how do i stop", + "what action should", + "what should i do", + "when should i", + "why does", + "why did", + "why is", "do you know", ) if not any(trigger in lower for trigger in direct_triggers): @@ -5757,69 +6512,81 @@ def fusion_component_export_direct_answer(messages): PRINTING_PROFILE_PARAMETER_STARTS = { "pla": { - "nozzleTemp": "200-220 C", - "bedTemp": "45-60 C", - "fan": "80-100%", - "flowRatio": "0.98-1.02 starting point", + "nozzleTemp": "205-220 C", + "bedTemp": "50-60 C", + "chamber": "open or cool enclosure", + "fan": "80-100% after first layers", + "flowRatio": "0.96-1.00 starting point", "pressureAdvance": "0.015-0.040 starting range for direct drive", - "maxVolumetric": "12-22 mm3/s depending on hotend", - "notes": "Tune temperature first, then flow, then pressure advance.", + "maxVolumetric": "12-20 mm3/s until tested", + "retraction": "direct drive 0.4-0.8 mm at 25-40 mm/s", + "notes": "Use strong part cooling; tune temp and flow before chasing stringing.", }, "petg": { - "nozzleTemp": "230-255 C", + "nozzleTemp": "235-255 C", "bedTemp": "70-85 C", - "fan": "20-60%", - "flowRatio": "0.96-1.00 starting point", + "chamber": "open or mild enclosure", + "fan": "20-50%, more for bridges", + "flowRatio": "0.95-1.00 starting point", "pressureAdvance": "0.020-0.060 starting range for direct drive", - "maxVolumetric": "8-16 mm3/s until tested", - "notes": "Reduce overcooling and avoid over-squish.", + "maxVolumetric": "8-14 mm3/s until tested", + "retraction": "direct drive 0.5-1.0 mm at 25-35 mm/s", + "notes": "Avoid over-squish and excessive fan; dry before stringing tests.", }, "pctg": { - "nozzleTemp": "245-270 C", - "bedTemp": "70-90 C", - "fan": "20-60%", - "flowRatio": "0.96-1.00 starting point", + "nozzleTemp": "250-270 C", + "bedTemp": "75-90 C", + "chamber": "mild enclosure if available", + "fan": "20-45%, more only for bridges/details", + "flowRatio": "0.95-1.00 starting point", "pressureAdvance": "0.020-0.060 starting range for direct drive", - "maxVolumetric": "8-14 mm3/s until tested", - "notes": "Treat as a tough PETG-family material and tune stringing carefully.", + "maxVolumetric": "7-12 mm3/s until tested", + "retraction": "direct drive 0.5-1.0 mm at 25-35 mm/s", + "notes": "Treat it like a tougher PETG family material: dry, moderate fan, tune flow carefully.", }, "abs": { "nozzleTemp": "245-270 C", - "bedTemp": "90-110 C", - "chamber": "45-60 C enclosed", - "fan": "0-25% except bridges/details", - "flowRatio": "0.98-1.02 starting point", - "pressureAdvance": "0.020-0.060 starting range", - "maxVolumetric": "8-16 mm3/s until tested", - "notes": "Heat soak and enclosure stability matter more than fan speed.", + "bedTemp": "95-110 C", + "chamber": "45-60 C enclosed if possible", + "fan": "0-20%, bridge/detail assist only", + "flowRatio": "0.96-1.00 starting point", + "pressureAdvance": "0.015-0.050 starting range for direct drive", + "maxVolumetric": "8-14 mm3/s until tested", + "retraction": "direct drive 0.4-0.8 mm at 25-40 mm/s", + "notes": "Heat soak the chamber and keep airflow low to avoid warping.", }, "asa": { "nozzleTemp": "250-275 C", - "bedTemp": "90-110 C", - "chamber": "45-60 C enclosed", - "fan": "0-30% except bridges/details", - "flowRatio": "0.98-1.02 starting point", - "pressureAdvance": "0.020-0.060 starting range", - "maxVolumetric": "8-16 mm3/s until tested", - "notes": "Best default choice for outdoor sun/weather parts.", - }, - "pa": { - "nozzleTemp": "255-290 C", - "bedTemp": "70-100 C", - "fan": "0-35%", + "bedTemp": "95-110 C", + "chamber": "45-60 C enclosed if possible", + "fan": "0-25%, bridge/detail assist only", "flowRatio": "0.96-1.00 starting point", - "pressureAdvance": "0.020-0.070 starting range", - "maxVolumetric": "6-12 mm3/s until tested", - "notes": "Dry hard and print from a drybox.", + "pressureAdvance": "0.015-0.050 starting range for direct drive", + "maxVolumetric": "8-14 mm3/s until tested", + "retraction": "direct drive 0.4-0.8 mm at 25-40 mm/s", + "notes": "Best outdoor default; tune with enclosure heat stable before flow and PA.", }, - "pa-cf": { - "nozzleTemp": "275-310 C", - "bedTemp": "80-110 C", - "fan": "0-35%", - "flowRatio": "0.94-0.99 starting point", - "pressureAdvance": "0.020-0.070 starting range", - "maxVolumetric": "5-11 mm3/s until tested", - "notes": "Use hardened nozzle, drybox, and design for anisotropic strength.", + "pa": { + "nozzleTemp": "260-290 C", + "bedTemp": "70-90 C", + "chamber": "enclosure helpful, not always hot", + "fan": "0-30%, bridge/detail assist only", + "flowRatio": "0.95-1.00 starting point", + "pressureAdvance": "0.020-0.070 starting range for direct drive", + "maxVolumetric": "5-10 mm3/s until tested", + "retraction": "direct drive 0.5-1.0 mm at 20-35 mm/s", + "notes": "Print from drybox; moisture ruins profile tuning quickly.", + }, + "pa-cf": { + "nozzleTemp": "280-310 C", + "bedTemp": "80-100 C", + "chamber": "45-60 C enclosed if available", + "fan": "0-30%, bridge/detail assist only", + "flowRatio": "0.95-1.00 starting point", + "pressureAdvance": "0.020-0.070 starting range for direct drive", + "maxVolumetric": "5-10 mm3/s until tested", + "retraction": "direct drive 0.4-0.8 mm at 20-35 mm/s", + "notes": "Use hardened nozzle and drybox; prioritize layer strength over glossy finish.", }, "pet-cf": { "nozzleTemp": "270-300 C", @@ -5833,24 +6600,26 @@ def fusion_component_export_direct_answer(messages): "notes": "Use hardened nozzle, dry the spool, and do not substitute PETG-CF values.", }, "pc": { - "nozzleTemp": "275-310 C", - "bedTemp": "100-120 C", - "chamber": "50-70 C enclosed", - "fan": "0-25%", + "nozzleTemp": "280-320 C", + "bedTemp": "100-115 C", + "chamber": "50-70 C enclosed if available", + "fan": "0-20%, bridge/detail assist only", "flowRatio": "0.96-1.00 starting point", - "pressureAdvance": "0.020-0.060 starting range", - "maxVolumetric": "5-12 mm3/s until tested", - "notes": "Needs enclosure, bed adhesion, and controlled cooling.", + "pressureAdvance": "0.015-0.050 starting range for direct drive", + "maxVolumetric": "5-10 mm3/s until tested", + "retraction": "direct drive 0.4-0.8 mm at 20-35 mm/s", + "notes": "Bed adhesion and warp control dominate; dry thoroughly.", }, "tpu": { "nozzleTemp": "220-245 C", "bedTemp": "35-60 C", - "fan": "30-80%", - "flowRatio": "1.00 starting point", - "pressureAdvance": "often off or very low until tested", + "chamber": "open or cool enclosure", + "fan": "30-70% depending on bridges and hardness", + "flowRatio": "0.98-1.03 starting point", + "pressureAdvance": "0.020-0.080 starting range; tune gently", "maxVolumetric": "2-6 mm3/s until tested", - "retraction": "minimal; slow direct-drive extrusion", - "notes": "Slow down and avoid long Bowden-style paths.", + "retraction": "direct drive 0.2-0.8 mm, slow; reduce if jams appear", + "notes": "Slow down and avoid high path friction through AMS/CFS-style systems.", }, } @@ -6135,6 +6904,30 @@ def matching_components(text): ) +PROFILE_CREATION_TERMS = ( + "create", + "make", + "generate", + "build", + "write", +) + + +PROFILE_PULL_INTENT_TERMS = ( + "pull", + "current", + "existing", + "read", + "show", + "settings", + "parameters", + "peramiter", + "peramiters", + "perameter", + "perameters", +) + + PROFILE_MATERIAL_ALIASES = { "pet-cf": ("pet-cf", "pet cf", "petcf"), "petg-cf": ("petg-cf", "petg cf", "petgcf"), @@ -6227,6 +7020,90 @@ def is_filament_profile_pull_request(messages): text = query.lower() if not query or not text_has_any(text, PROFILE_PULL_TERMS): return False + if text_has_any(text, ("fusion 360", "fusion", "cad", "sketch", "user parameter", "user parameters")) and not text_has_any( + text, + ( + "orca", + "orcaslicer", + "tinmanx", + "tinmanx1", + "filament profile", + "process profile", + "machine profile", + "printer profile", + "current profile", + "current settings", + "pull", + ), + ): + return False + if text_has_any(text, PROFILE_CREATION_TERMS) and not text_has_any(text, PROFILE_PULL_INTENT_TERMS): + return False + specific_profile_target = bool( + matching_printer_profiles(text) + or matching_materials(text) + or text_has_any( + text, + ( + "qidi", + "bambu", + "creality", + "sovol", + "rat rig", + "ratrig", + "snapmaker", + "centauri", + "x1c", + "h2d", + "k2 plus", + "plus 4", + "sv08", + ), + ) + ) + strong_pull_intent = text_has_any( + text, + ( + "pull", + "current", + "existing", + "actual local", + "local slicer profile", + "profile data", + "read the profile", + "show me the profile", + ), + ) + explicit_specific_parameters = specific_profile_target and text_has_any( + text, + ( + "filament profile parameters", + "filament profile settings", + "filament settings", + "profile parameters", + "profile settings", + ), + ) + conceptual_profile_question = text_has_any( + text, + ( + "how do i tell whether", + "how do i decide whether", + "what do machine", + "what do filament", + "what do process", + "what controls", + "which profile controls", + "what settings matter", + "settings matter most", + "what filament settings matter", + "bad print is caused by", + ), + ) + if conceptual_profile_question and not strong_pull_intent: + return False + if not specific_profile_target and not strong_pull_intent and not explicit_specific_parameters: + return False profile_context = ( "orca", "orcaslicer", @@ -6251,6 +7128,93 @@ def is_filament_profile_pull_request(messages): return bool(matching_printer_profiles(text) or matching_materials(text)) +def is_orca_nozzle_visibility_question(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query or "nozzle" not in text: + return False + slicer_context = ( + "orca", + "orcaslicer", + "orca slicer", + "prepare tab", + "tinmanx", + "tinmanx1", + "filament type", + "filament color", + ) + visibility_terms = ( + "installed", + "see", + "show", + "display", + "visible", + "visibility", + "sync", + "options", + "what are my options", + "what type", + "which type", + ) + return text_has_any(text, slicer_context) and text_has_any(text, visibility_terms) + + +def is_orca_slicer_workflow_question(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query or not text_has_any(text, ("orca", "orcaslicer", "tinmanx", "slicer")): + return False + workflow_terms = ( + "http 405", + "405 not allowed", + "405", + "device tab", + "devive tab", + "white screen", + "slice hits", + "slice hit", + "slice stalls", + "slice stall", + "stalls", + "network printing", + "print to my", + "printer connection", + "prepare tab", + "facelift", + "new look", + "theme", + "branding", + "opening tile", + "colors", + "black", + "green", + "workflow of orca", + ) + return text_has_any(text, workflow_terms) + + +def is_fusion_cam_question(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query or not text_has_any(text, ("fusion", "autodesk fusion")): + return False + return text_has_any( + text, + ( + "manufacture workspace", + "2d contour", + "2d adaptive", + "toolpath", + "stock + shoulder", + "stock shoulder", + "simulate", + "simulation", + "cnc run", + "cam", + ), + ) + + def profile_query_material_key(text): padded = f" {text.lower()} " for key, aliases in PROFILE_MATERIAL_ALIASES.items(): @@ -6294,6 +7258,242 @@ def profile_query_printer_aliases(text): return tuple(dict.fromkeys(aliases)) +def is_orca_profile_creation_request(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query or not text_has_any(text, PROFILE_CREATION_TERMS): + return False + if not text_has_any(text, ("orca", "orcaslicer", "tinmanx1", "slicer")): + return False + return text_has_any(text, ("filament profile", "material profile", "profile", "profiles")) and ( + "filament" in text or bool(profile_query_material_key(text)) + ) + + +def orca_profile_creation_material_key(text): + material_key = profile_query_material_key(text) + if material_key: + return material_key + if "filament" in text.lower(): + return "petg" + return "" + + +def orca_profile_creation_nozzles(text): + nozzle = profile_query_nozzle(text) + if nozzle: + return [nozzle] + lower = text.lower() + if text_has_any(lower, ("all nozzle", "all nozzles", "all nozzle sizes", "every nozzle")): + return ["0.4", "0.6", "0.8"] + return ["0.4", "0.6"] + + +def orca_profile_creation_printers(text): + lower = text.lower() + if text_has_any(lower, ("all printer", "all printers", "every printer", "my printers")): + return list(PRINTING_PRINTER_PROFILES) + profiles = matching_printer_profiles(lower) + return profiles or list(PRINTING_PRINTER_PROFILES) + + +def numeric_range_values(value): + return [float(item) for item in re.findall(r"\d+(?:\.\d+)?", str(value or ""))] + + +def choose_profile_number(value, fallback, decimals=2): + numbers = numeric_range_values(value) + if len(numbers) >= 2: + result = (numbers[0] + numbers[1]) / 2.0 + elif len(numbers) == 1: + result = numbers[0] + else: + result = float(fallback) + if decimals == 0: + return str(int(round(result))) + return f"{result:.{decimals}f}".rstrip("0").rstrip(".") + + +def orca_starter_values(material_key, nozzle): + starter = PRINTING_PROFILE_PARAMETER_STARTS.get(material_key) or PRINTING_PROFILE_PARAMETER_STARTS["petg"] + nozzle_temp = int(float(choose_profile_number(starter.get("nozzleTemp"), 245, decimals=0))) + bed_temp = int(float(choose_profile_number(starter.get("bedTemp"), 80, decimals=0))) + fan_numbers = numeric_range_values(starter.get("fan")) + fan_min = int(round(fan_numbers[0])) if fan_numbers else 20 + fan_max = int(round(fan_numbers[1])) if len(fan_numbers) >= 2 else max(fan_min, 45) + max_volumetric = float(choose_profile_number(starter.get("maxVolumetric"), 10, decimals=2)) + try: + nozzle_float = float(nozzle) + except Exception: + nozzle_float = 0.4 + if nozzle_float >= 0.8: + max_volumetric *= 1.35 + elif nozzle_float >= 0.6: + max_volumetric *= 1.18 + pressure_advance = choose_profile_number(starter.get("pressureAdvance"), 0.04, decimals=3) + retraction = choose_profile_number(starter.get("retraction"), 0.7, decimals=2) + flow_ratio = choose_profile_number(starter.get("flowRatio"), 0.98, decimals=3) + return { + "nozzleTemp": str(nozzle_temp), + "initialNozzleTemp": str(nozzle_temp + 5), + "bedTemp": str(bed_temp), + "fanMin": str(fan_min), + "fanMax": str(fan_max), + "maxVolumetric": f"{max_volumetric:.2f}".rstrip("0").rstrip("."), + "pressureAdvance": pressure_advance, + "flowRatio": flow_ratio, + "retraction": retraction, + "notes": starter.get("notes", ""), + } + + +def stage_orca_profile_pack(messages, target_path=None): + query = latest_user_text(messages).strip() + text = query.lower() + material_key = orca_profile_creation_material_key(text) + if not material_key: + raise ValueError("No filament material was detected for the Orca profile pack.") + material = PRINTING_MATERIAL_LIBRARY.get(material_key, {}) + material_name = material.get("name") or material_key.upper() + printers = orca_profile_creation_printers(text) + nozzles = orca_profile_creation_nozzles(text) + slug = slugify(f"orca-{material_key}-profile-pack", "orca-profile-pack") + target = Path(target_path).expanduser() if target_path else LOCAL_ORCA_PROFILE_OUTPUT_DIR / f"{time.strftime('%Y%m%d-%H%M%S')}-{slug}" + target.mkdir(parents=True, exist_ok=True) + profiles_dir = target / "profiles" + profiles_dir.mkdir(parents=True, exist_ok=True) + + created = [] + matrix_rows = [["printer", "nozzle_mm", "profile_name", "path", "nozzle_temp_c", "bed_temp_c", "fan_min", "fan_max", "max_volumetric_mm3_s", "pressure_advance", "flow_ratio"]] + for printer in printers: + printer_name = printer.get("name") or "Printer" + for nozzle in nozzles: + values = orca_starter_values(material_key, nozzle) + profile_name = f"Codex {material_name} Starter @{printer_name} {nozzle} nozzle" + profile = { + "type": "filament", + "from": "User", + "name": profile_name, + "filament_settings_id": [profile_name], + "filament_type": [material_key.upper()], + "filament_vendor": ["Codex Starter"], + "filament_flow_ratio": [values["flowRatio"]], + "nozzle_temperature": [values["nozzleTemp"]], + "nozzle_temperature_initial_layer": [values["initialNozzleTemp"]], + "nozzle_temperature_range_low": [values["nozzleTemp"]], + "nozzle_temperature_range_high": [values["initialNozzleTemp"]], + "textured_plate_temp": [values["bedTemp"]], + "textured_plate_temp_initial_layer": [values["bedTemp"]], + "hot_plate_temp": [values["bedTemp"]], + "hot_plate_temp_initial_layer": [values["bedTemp"]], + "fan_min_speed": [values["fanMin"]], + "fan_max_speed": [values["fanMax"]], + "close_fan_the_first_x_layers": ["3"], + "slow_down_layer_time": ["6"], + "enable_pressure_advance": ["1"], + "pressure_advance": [values["pressureAdvance"]], + "filament_max_volumetric_speed": [values["maxVolumetric"]], + "filament_retraction_length": [values["retraction"]], + "filament_retraction_speed": ["30"], + "compatible_printers": [f"{printer_name} {nozzle} nozzle"], + "compatible_printers_condition": "", + "notes": values["notes"], + "version": "2.3.1.10", + } + path = profiles_dir / f"{slugify(profile_name, 'orca-profile')}.json" + path.write_text(json.dumps(profile, indent=2), encoding="utf-8") + created.append({"printer": printer_name, "nozzle": nozzle, "name": profile_name, "path": str(path), "values": values}) + matrix_rows.append( + [ + printer_name, + nozzle, + profile_name, + str(path), + values["nozzleTemp"], + values["bedTemp"], + values["fanMin"], + values["fanMax"], + values["maxVolumetric"], + values["pressureAdvance"], + values["flowRatio"], + ] + ) + + def csv_cell(value): + return '"' + str(value).replace('"', '""') + '"' + + matrix_path = target / "profile_matrix.csv" + matrix_path.write_text("\n".join(",".join(csv_cell(cell) for cell in row) for row in matrix_rows) + "\n", encoding="utf-8") + readme_path = target / "README.md" + readme_path.write_text( + "\n".join( + [ + f"# Orca {material_name} Starter Profile Pack", + "", + f"Created from prompt: {query}", + "", + "These are starter filament profiles, not final tuned profiles.", + "Run Orca calibrations in this order before treating them as production: temperature, max volumetric speed, pressure advance, flow, retraction, then tolerance if fit matters.", + "", + f"Profiles created: {len(created)}", + f"Printers: {', '.join(dict.fromkeys(item['printer'] for item in created))}", + f"Nozzles: {', '.join(nozzles)} mm", + "", + "Files:", + f"- Matrix: {matrix_path}", + f"- Profiles folder: {profiles_dir}", + "", + ] + ), + encoding="utf-8", + ) + return { + "ok": True, + "targetDir": str(target), + "profilesDir": str(profiles_dir), + "readmePath": str(readme_path), + "matrixPath": str(matrix_path), + "materialKey": material_key, + "materialName": material_name, + "printerCount": len(printers), + "nozzles": nozzles, + "profileCount": len(created), + "profiles": created, + } + + +def orca_profile_creation_direct_answer(messages): + if not is_orca_profile_creation_request(messages): + return "" + result = stage_orca_profile_pack(messages) + sample = result.get("profiles", [])[:4] + sample_lines = "\n".join(f"- {item['name']}: `{item['path']}`" for item in sample) + more = result.get("profileCount", 0) - len(sample) + if more > 0: + sample_lines += f"\n- plus {more} more profiles in `{result.get('profilesDir')}`" + return "\n\n".join( + [ + f"I created an Orca {result.get('materialName')} starter profile pack with {result.get('profileCount')} profiles.", + "\n".join( + [ + f"Profile pack: `{result.get('targetDir')}`", + f"Matrix CSV: `{result.get('matrixPath')}`", + f"README: `{result.get('readmePath')}`", + "Sample profiles:", + sample_lines, + ] + ), + ( + "This is why: this was a create-profile request, not a pull-current-settings request. " + "I generated starter filament JSONs per printer/nozzle pair and kept the profile pack separate from installed Orca data so it can be inspected before import." + ), + ( + "You should also consider: these are not perfect production profiles yet. Use Orca calibration to tune temperature, max volumetric speed, pressure advance, flow, retraction, and fit/tolerance for each real printer, nozzle, filament brand, and drying condition." + ), + ] + ) + + def slicer_profile_roots(): home = Path.home() return [ @@ -6302,6 +7502,7 @@ def slicer_profile_roots(): ("TinManX1 user app profile library", home / "Applications/TinManX1.app/Contents/Resources/profiles", 106), ("OrcaSlicer", home / "Library/Application Support/OrcaSlicer/user", 95), ("Desktop 3D Printing profiles", home / "Desktop/3D Printing", 85), + ("Generated Orca profiles", home / "Applications" / "generated_orca_profiles", 75), ("PrusaSlicer", home / "Library/Application Support/PrusaSlicer", 45), ] @@ -7171,6 +8372,431 @@ def orca_calibration_visual_direct_answer(messages): ) +def orca_nozzle_visibility_direct_answer(messages): + if not is_orca_nozzle_visibility_question(messages): + return "" + return "\n\n".join( + [ + "Best path: show the configured nozzle from the Orca/TinmanX1 printer profile immediately, and show the installed physical nozzle as a separate verified field.", + ( + "This is why: Orca always knows the selected/configured printer profile nozzle, but most printers do not automatically know what brass, hardened steel, ruby, or CHT-style nozzle is physically installed. " + "If we blend those together, the UI can confidently show the wrong nozzle and create bad filament/process choices." + ), + ( + "Your practical options are: add printer variants per nozzle size and type in Orca; keep a local machine inventory with the last-confirmed installed nozzle and material; " + "for Klipper machines, optionally expose a custom saved variable or macro value that TinmanX1 can read; and mark the field as unverified whenever there is no trusted source." + ), + ( + "You should also consider: use two labels in the UI: `Configured nozzle` from Orca and `Installed physical nozzle` from the local inventory or verified printer source. " + "That keeps the workflow honest while still giving you the nozzle visibility you want in the Prepare tab." + ), + ] + ) + + +def orca_http_405_direct_answer(messages): + query = latest_user_text(messages).lower() + if not ("405" in query and text_has_any(query, ("orca", "orcaslicer", "slicer"))): + return "" + return "\n\n".join( + [ + "First check the Orca printer-host type and upload URL; HTTP 405 means Orca reached something, but used an endpoint or method that printer does not allow.", + ( + "This is why: a 405 is different from offline or bad-password. It usually means the printer web service answered, but Orca is using the wrong host preset, port, path, or upload method for that firmware. " + "For a Creality K2 Plus, do not assume it behaves exactly like generic Klipper/Moonraker unless the firmware or Open API confirms that." + ), + ( + "Fix path: in Orca, edit the physical printer connection, confirm the printer type/API mode, remove any guessed upload path, test the bare host/port first, then try the firmware-supported upload endpoint. " + "If Creality's LAN service rejects generic upload, use the Creality/Orca-supported host preset or export G-code locally until the correct endpoint is verified." + ), + ( + "You should also consider: capture the exact URL, port, and Orca printer-host preset that produced the 405. " + "That gives the next step a real target instead of guessing at random Moonraker, OctoPrint, or Creality endpoints." + ), + ] + ) + + +def tinmanx_slice_stall_direct_answer(messages): + query = latest_user_text(messages).lower() + if not (text_has_any(query, ("tinmanx", "orca", "slicer")) and "slice" in query and text_has_any(query, ("80%", "80 percent", "stalls", "stall"))): + return "" + return "\n\n".join( + [ + "First reproduce the 80% stall with logging on, then isolate whether it is the model, supports, profile, or system resources.", + ( + "This is why: slicers often hit the heaviest geometry/support/toolpath work late in the progress bar. " + "An 80% stall is usually not one magic setting; it is commonly a bad mesh, pathological supports, excessive modifiers, disk/RAM pressure, or a slicer-engine crash hidden behind the progress UI." + ), + ( + "Fix path: try the same model with a stock profile, then disable supports, then repair/simplify the mesh, then clear TinManX/Orca temp/cache files. " + "If it still stalls, collect the slicing log and the smallest model/profile pair that reproduces it so the fix can target the actual failure." + ), + ( + "You should also consider: if only one model stalls, repair the STL/3MF first. If every model stalls around 80%, treat it as an app/cache/resource issue and run a clean-profile slice before changing print settings." + ), + ] + ) + + +def orca_device_tab_control_direct_answer(messages): + query = latest_user_text(messages).lower() + if not (text_has_any(query, ("orca", "orcaslicer", "slicer")) and text_has_any(query, ("device tab", "devive tab", "more control"))): + return "" + return "\n\n".join( + [ + "Best answer: yes, you can add more control in Orca's Device tab only if the printer firmware exposes a compatible local control interface.", + ( + "This is why: Orca is mostly a slicer plus a web/device panel. A standard Klipper printer feels powerful there because Moonraker/Mainsail/Fluidd expose rich controls. " + "Open Centauri may expose some controls, but it is not automatically the same as a full standard Klipper stack." + ), + ( + "Your options are: use whatever Open Centauri web UI already exposes in the Device tab; add or embed a richer local web interface if the firmware supports it; " + "or keep advanced control in the printer's native/Open Centauri UI while Orca handles slicing and upload." + ), + ( + "You should also consider: choose the control path based on the confirmed firmware API. If Open Centauri exposes Moonraker-compatible endpoints, we can integrate deeper. If it does not, forcing generic Klipper controls will create brittle buttons." + ), + ] + ) + + +def fusion_cam_stock_shoulder_direct_answer(messages): + if not is_fusion_cam_question(messages): + return "" + return "\n\n".join( + [ + "First check the tool, stock boundary, and 2D Contour heights; `Stock + Shoulder` means Fusion thinks the cutter shoulder or holder will collide with the stock during simulation.", + ( + "This is why: in Manufacture, Fusion is not only checking the cutting flute. It also checks the non-cutting shoulder/holder against the remaining stock and setup clearance. " + "A small contour, oversized tool, short flute length, tight stock boundary, or wrong top/bottom height can trigger it." + ), + ( + "Fix path: verify the tool flute length and shoulder/holder dimensions, enlarge or correctly define the stock, set the contour boundary/offset so the tool has room, and check Heights so the top, retract, and clearance planes are sane. " + "Then simulate again with stock visibility and tool holder collision enabled." + ), + ( + "You should also consider: if the geometry is correct but the warning remains, switch to a longer-flute tool, add a roughing pass, use multiple stepdowns, or move the contour away from the stock edge." + ), + ] + ) + + +def ai_print_failure_monitoring_direct_answer(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query: + return "" + if not ("ai" in text and "print" in text and "failure" in text and text_has_any(text, ("monitor", "detection", "detect"))): + return "" + return "\n\n".join( + [ + "Best free/local starting point: self-hosted Obico with a printer camera, then add better camera placement and alert tuning before buying anything.", + ( + "This is why: Obico has the broadest open/self-hosted path for AI print-failure detection across OctoPrint and Klipper/Moonraker-style workflows. " + "It can watch the camera feed, flag spaghetti/adhesion failures, and notify you without depending on a paid cloud service if you host it yourself." + ), + ( + "You should also consider: AI monitoring is only as good as the camera view and lighting. Put the full build plate in frame, avoid glare, tune sensitivity, and treat it as an early-warning system rather than a guaranteed stop button." + ), + ] + ) + + +def aircraft_wood_defect_direct_answer(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query: + return "" + if not ("wood" in text and "aircraft" in text and "structural repair" in text and "mineral streak" in text): + return "" + return "\n\n".join( + [ + "Pick C: mineral streaks are acceptable only when they are not accompanied by decay.", + ( + "This is why: compression failures and splits are structural defects because they reduce load-carrying capacity and create failure paths. " + "A mineral streak by itself is a discoloration/appearance defect, not automatically a strength defect." + ), + ( + "You should also consider: inspect for decay, moisture damage, checks, shakes, compression wrinkles, and grain problems before approving the wood. " + "If this is for a certificated aircraft repair, follow the applicable aircraft manual or accepted wood-repair guidance rather than treating the quiz answer as repair authorization." + ), + ] + ) + + +def flightops_service_charge_no_activity_direct_answer(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query: + return "" + if not (text_has_any(text, ("service charge", "service fee", "services charge", "services charges")) and text_has_any(text, ("no flights", "no flight activity", "had no flights"))): + return "" + return "\n\n".join( + [ + "Yes. The fix is to make monthly service charges render even when the customer has zero flight rows for that aircraft/month.", + ( + "This is why: the report logic is probably anchored on flight activity, so a customer/month with no flights never gets a section where the standalone service fee can be attached. " + "For N411GC in February, WB Air 2021 still needs a billable line even though there are no Hobbs/flight entries." + ), + ( + "Fix path: build the report's customer/month set from flights plus billable non-flight charges, then left-join or merge flight rows into that set. " + "Add a regression case for N411GC / February / WB Air 2021 with zero flights and one service charge, and verify the PDF/report total includes the charge." + ), + ( + "You should also consider: keep the empty-flight section visually clear so it does not look like missing data. " + "A label like `No flight activity this period` plus the service-charge line keeps accounting honest." + ), + ] + ) + + +def tinmanx_ui_theme_direct_answer(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query: + return "" + if not text_has_any(text, ("tinmanx", "orca", "rocket slicer", "bambu slicer")): + return "" + if not text_has_any(text, ("facelift", "new look", "colors", "black", "green", "opening tile", "branding", "theme")): + return "" + return "\n\n".join( + [ + "Yes. Treat this as a TinManX source-level theme/branding change, not a user Orca config tweak.", + ( + "This is why: changing black-to-blue, green-to-red, the launch tile name, and the overall Rocket/Bambu-style polish needs to happen in the app assets, styles, resources, and branding strings. " + "Editing a local Orca preference file would be fragile and would not travel cleanly with the TinManX app or installer." + ), + ( + "Fix path: update the theme palette, logo/assets, splash/opening tile text, app display strings, and any bundled resources; then launch the installed app and screenshot the Prepare/Device/opening views. " + "Set the opening tile/app label to `TinManX 1.0.0` and keep the Orca workflow intact while only changing the visual layer and TinManX naming." + ), + ( + "You should also consider: make the palette accessible, not just different. Blue/red should still have enough contrast in dark and light modes, and green-to-red should not be the only status cue for users with color-vision limitations." + ), + ] + ) + + +def codex_ui_workflow_scenario_direct_answer(messages): + query = latest_user_text(messages).strip() + text = query.lower() + required = ("tinmanx1", "polymaker", "fiberon", "steer", "edit", "github", "zip") + if not query or not all(term in text for term in required): + return "" + return "\n\n".join( + [ + ( + "Yes. I would handle this as one release-grade Codex CLI UI upgrade: add the Polymaker/Fiberon preset request as a regression scenario, " + "load the 100 CAD/CNC questions into the public test bank, wire Steer and Edit controls into the chat actions, connect `Fix this` to the self-healing queue, " + "then run package health and golden batches before the final GitHub zip." + ), + ( + "This is why: the weak point is not one answer template; it is the loop. The app needs to capture real requests, classify the work, let Tinman steer or edit without starting over, " + "turn wrong answers into self-healing code/test candidates, and only publish a package after checks pass." + ), + ( + "You should also consider: public zip packaging should include the app, scripts, fixtures, docs, and optional free-tool installer manifests, but not private chat history, machine credentials, " + "API keys, or third-party binaries unless their license and size make redistribution safe." + ), + ] + ) + + +def inverter_three_phase_direct_answer(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query: + return "" + if not ("inverter" in text and "3 phase" in text and text_has_any(text, ("split phase", "120v/240v", "120/240", "120 v/240 v", "vevor"))): + return "" + return "\n\n".join( + [ + "My recommendation: do not feed that VEVOR 120V/240V split-phase inverter directly from a 3-phase AC input.", + ( + "This is why: `120V/240V split phase` is North American single/split-phase service, not three-phase service. " + "If the unit does not explicitly list 3-phase AC input, L1/L2/L3 input, or a compatible three-phase voltage range, treat it as not 3-phase compatible." + ), + ( + "You should also consider: verify the exact manual label for AC input before wiring. " + "If your source is truly 3-phase, you need the correct transformer/converter or a hybrid inverter that explicitly supports your three-phase voltage and grounding system." + ), + ] + ) + + +def aero_tool_install_direct_answer(messages): + query = latest_user_text(messages).strip() + text = query.lower() + tool_terms = ("xfoil", "openvsp", "su2", "qblade") + if not query or not all(term in text for term in tool_terms): + return "" + if not text_has_any(text, ("how do we get", "how do i get", "install", "download", "set up", "setup")): + return "" + return "\n\n".join( + [ + "My recommendation: install them in this order on macOS: OpenVSP, XFOIL, SU2, then QBlade through the cleanest Linux path if the native Mac build fights us.", + ( + "This is why: OpenVSP gives geometry and aircraft layout fastest, XFOIL gives quick airfoil polars, SU2 gives serious CFD capability, and QBlade is useful but has the most packaging friction on macOS. " + "That order gives Tinman useful aero capability quickly without blocking on the hardest installer first." + ), + ( + "You should also consider: keep each install visible in the tool inventory with command paths and a tiny smoke test. " + "For example: launch OpenVSP, run an XFOIL polar, verify `SU2_CFD --help`, and keep QBlade isolated if it needs Linux or a VM/container." + ), + ] + ) + + +def vague_failure_diagnostic_direct_answer(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query: + return "" + if not ("failure" in text and text_has_any(text, ("can you see", "what failure", "what failed", "had a failure", "believe i had"))): + return "" + return "\n\n".join( + [ + "My recommendation: start with the most recent local app/log failure around the time you saw the issue, then work backward through the active project logs instead of guessing.", + ( + "This is why: a vague failure report can come from the UI, local worker, slicer, printer endpoint, build script, or test runner. " + "The fastest safe diagnostic path is read-only: check the newest app log, crash log, golden-batch result, and any printer/slicer log tied to that project before changing settings." + ), + ( + "You should also consider: if the name is misspelled, treat `cantauri Tinman` as likely Centauri/Tinman context, then verify the exact app/project from logs. " + "First checks: latest timestamp, exact error line, route/project, command that ran, and whether a final answer or artifact was produced." + ), + ] + ) + + +def domain_sample_direct_answer(messages, route=None): + query = latest_user_text(messages).strip() + text = query.lower() + if not query: + return "" + category = "" + question = query + for prefix in ( + "3D Printing", + "CAD", + "CNC Machining", + "Solar And Wind Technology", + "Aerodynamics", + "CFD Analysis", + "Engineering", + "Aviation", + ): + marker = prefix + ":" + if query.lower().startswith(marker.lower()): + category = prefix + question = query[len(marker):].strip() + break + if not category: + return "" + + lower_question = question.lower() + if category == "3D Printing": + if text_has_any(lower_question, ("material", "pla", "petg", "asa", "nylon", "carbon")): + lead = "Pick the filament from the job first: PLA for easy indoor prototypes, PETG for general utility, ASA for outdoor UV/weather, ABS/ASA for heat, nylon for toughness, and CF-filled materials for stiffness when abrasion and cost are acceptable." + elif text_has_any(lower_question, ("warping", "stringing", "under-extruding", "layer-shifting", "fail")): + lead = "Start with the failure mode: warping is usually heat/adhesion/cooling, stringing is temperature/retraction/moisture, under-extrusion is flow/path restriction, and layer shifting is usually mechanical or acceleration related." + elif text_has_any(lower_question, ("orient", "supports", "redesigned", "tolerances", "strong enough", "strength")): + lead = "Start from load direction and manufacturing limits: orient layers so the main tensile load does not peel along layer lines, then redesign support-heavy features into chamfers, bridges, split parts, or bolt-on features." + else: + lead = "Start with material, printer capability, part load, heat, UV, tolerance, and surface-finish requirements before choosing settings." + why = "This is why: 3D printing failures and material choices are coupled. Material, drying, nozzle size, layer orientation, wall count, chamber temperature, cooling, and slicer calibration all change strength and finish." + consider = "You should also consider: give me the part use, dimensions, load direction, environment, printer, nozzle, and filament brand when you want a final setting or design call." + elif category == "CAD": + if text_has_any(lower_question, ("drawing from a 3d model", "technical drawing package", "exploded views", "assembly drawings")): + lead = "Start from the 3D model, then create only the drawing views needed to manufacture and inspect the part: base view, projected/section/detail views, critical dimensions, datums, tolerances, notes, material, finish, revision, and BOM when it is an assembly." + elif text_has_any(lower_question, ("datum", "gd&t", "tolerance", "dimension", "drawing")): + lead = "Start the CAD answer from function: choose datums from the surfaces that locate the part in the real assembly, then dimension only what controls fit, motion, sealing, inspection, or manufacturing." + elif text_has_any(lower_question, ("cnc", "machining", "manufacture", "manufacturable", "fillets", "chamfers", "holes", "thread", "sheet metal", "bend", "draft", "molding", "casting")): + lead = "Model the part around the process: tool access, minimum radii, stock shape, setup direction, wall thickness, fasteners, inspection surfaces, and realistic tolerances should drive the CAD." + elif text_has_any(lower_question, ("fea", "simulation", "strength", "load", "stiffness", "weight", "ribs", "gussets", "brackets")): + lead = "Build the model so the load path is visible: keep critical geometry accurate, simplify cosmetic detail for analysis, and define loads, constraints, material, and safety factor before trusting a strength call." + elif text_has_any(lower_question, ("assembly", "interfer", "clearance", "mates", "reference", "rebuild", "design intent", "revision")): + lead = "Make the model stable before making it fancy: use clean origin references, named parameters, simple sketches, controlled mates, interference checks, and a file structure another engineer can follow." + elif text_has_any(lower_question, ("mesh", "step", "stl", "dxf", "iges", "export")): + lead = "Choose the export by the next operation: STEP for editable solid exchange, STL/3MF for printing meshes, DXF for flat cutting, and IGES only when the receiving tool needs older surface data." + else: + lead = "Start with design intent, manufacturing process, datums, load path, mating interfaces, tolerances, and inspection method before adding detail." + why = "This is why: CAD is not just shape creation. Good models preserve intent, make manufacturing possible, keep assemblies predictable, and leave a clean path for drawings, CAM, simulation, and revision." + consider = "You should also consider: tell me the process, material, critical dimensions, load direction, mating parts, quantity, and inspection method when you want a final design recommendation." + elif category == "CNC Machining": + if text_has_any(lower_question, ("feeds", "speeds")): + lead = "Use feeds and speeds from the material, cutter diameter/flutes, tool material, stickout, machine rigidity, and operation; do not use one universal RPM/feed." + elif text_has_any(lower_question, ("3-axis", "4-axis", "5-axis", "3 axis", "4 axis", "5 axis")): + lead = "Pick the simplest axis count that reaches the features and tolerance: use 3-axis for accessible prismatic parts, 4-axis when rotation reduces setups or keeps features concentric, and 5-axis only when tool access, undercuts, compound angles, or setup reduction justify the cost." + elif text_has_any(lower_question, ("dimensional error", "dimensional errors", "troubleshoot")): + lead = "Troubleshoot dimensional errors in order: confirm the measurement method, then check work offset, tool length/diameter offsets, stock movement, cutter wear/deflection, thermal growth, CAM compensation, and whether roughing left enough finishing stock." + elif text_has_any(lower_question, ("chatter", "surface finish")): + lead = "Fix chatter and poor finish by checking rigidity first: workholding, tool stickout, cutter sharpness, chip load, spindle speed, radial engagement, coolant, and toolpath." + elif text_has_any(lower_question, ("milled", "turned", "laser", "waterjet", "3d printed")): + lead = "Choose the process by geometry and tolerance: turn round parts, mill precise 3D features, laser/waterjet flat profiles, and 3D print shapes that are hard to machine or need fast iteration." + else: + lead = "Start with material, tolerance, geometry, machine capability, tool access, and fixturing before choosing the manufacturing path." + why = "This is why: CNC success is mostly chip formation plus rigidity. A perfect CAD shape can still chatter, move in the fixture, burn tools, or cost too much if the process plan is wrong." + consider = "You should also consider: share material, stock size, machine type, tool list, tolerance, quantity, and whether the part is prototype or production." + elif category == "Solar And Wind Technology": + lead = "Start with the load in watt-hours per day, then size panels, batteries, charge controller, inverter, and wiring from that energy budget." + if "wind" in lower_question and "location" in lower_question: + lead = "Wind is only practical if your measured average wind speed and tower height are good; otherwise solar usually wins for reliability, cost, and maintenance." + why = "This is why: off-grid power is an energy-balance problem. Loads, sun hours, wind resource, battery chemistry, depth of discharge, temperature, inverter surge, and reserve days control the system size." + consider = "You should also consider: use real measured loads and local solar/wind data before buying hardware, then derate for weather, battery temperature, wiring losses, and equipment surge." + elif category == "Aerodynamics": + lead = "Start with speed, characteristic length, Reynolds number, angle of attack, and shape; those determine whether lift, drag, separation, or turbulence is the main problem." + why = "This is why: aerodynamic behavior changes with scale and flow regime. A shape that works at one Reynolds number or angle of attack may stall, separate, or create excess drag somewhere else." + consider = "You should also consider: use hand estimates first, then XFOIL/OpenVSP/OpenFOAM or a smoke/tuft test when geometry, speed, and boundary conditions are known." + elif category == "CFD Analysis": + lead = "Start by defining the physics, domain, boundary conditions, mesh strategy, turbulence model, convergence criteria, and validation check before trusting any CFD result." + why = "This is why: CFD can produce polished-looking wrong answers when the mesh, boundary conditions, turbulence model, or residual targets do not match the real flow." + consider = "You should also consider: compare CFD against a hand calculation, published correlation, wind-tunnel/smoke test, or simple pressure/flow measurement before calling the result real." + elif category == "Engineering": + lead = "Start with the actual load case, constraints, material, process, geometry, safety factor, and failure modes, then choose thickness, fasteners, welds, or redesign changes from that." + why = "This is why: strong enough is not one number. Static strength, fatigue, deflection, heat, creep, corrosion, fastener bearing, weld quality, and manufacturability can each control the design." + consider = "You should also consider: define what failure means, what safety factor is appropriate, and whether the design needs hand calculations, FEA, prototype testing, or all three." + elif category == "Aviation": + lead = "Use the aircraft's POH/AFM for operational numbers, and use aerodynamic principles to understand why the aircraft behaves that way." + if text_has_any(lower_question, ("lift", "flaps", "slats", "spoilers", "control", "stall", "spin", "yaw")): + lead = "Start with angle of attack, airflow over the wing/control surface, and energy state; those explain lift, stalls, spins, adverse yaw, and control effectiveness." + why = "This is why: aviation performance depends on weight, balance, density altitude, configuration, power, drag, wind, and pilot technique. Conceptual rules help, but aircraft-specific data controls real decisions." + consider = "You should also consider: separate educational analysis from flight planning, and verify real-world operation against the POH/AFM, regulations, training, and current conditions." + else: + return "" + if "recommend" not in lead.lower(): + lead = "My recommendation: " + lead[0].lower() + lead[1:] + return "\n\n".join([lead, why, consider]) + + +def general_direct_knowledge_answer(messages, route=None): + answer = ( + domain_sample_direct_answer(messages, route) + or inverter_three_phase_direct_answer(messages) + or aero_tool_install_direct_answer(messages) + or vague_failure_diagnostic_direct_answer(messages) + or ai_print_failure_monitoring_direct_answer(messages) + or aircraft_wood_defect_direct_answer(messages) + or flightops_service_charge_no_activity_direct_answer(messages) + or tinmanx_ui_theme_direct_answer(messages) + or codex_ui_workflow_scenario_direct_answer(messages) + ) + if not answer: + return None + route_id = (route or {}).get("projectId", "") + if route_id == "flightops-tracker": + mode = "flightops-direct-answer" + thought = "Recognized this as a FlightOps report/accounting logic issue and answering with the implementation target first." + elif route_id == "tinmanx-slicer-research": + mode = "tinmanx-ui-direct-answer" + thought = "Recognized this as a TinManX/Orca workflow or branding task, not a CAD/export question." + else: + mode = "direct-knowledge-answer" + thought = "Recognized this as a direct technical question, so I am answering first and keeping the reasoning compact." + return {"mode": mode, "thought": thought, "answer": answer} + + def wants_printing_expert_context(messages): text = printing_query_text(messages) terms = ( @@ -7288,6 +8914,32 @@ def component_manual_direct_answer(messages): ) +def toolboard_upgrade_decision_direct_answer(messages): + query = latest_user_text(messages).strip() + text = query.lower() + if not query or not text_has_any(text, ("ebb42", "ebb 42", "toolboard", "toolhead mcu")): + return "" + if not text_has_any(text, ("advisable", "should", "upgrade", "worth", "m5p", "runout")): + return "" + return "\n\n".join( + [ + "My recommendation: yes, upgrading to an EBB42 is worth considering if you are already adding an M5P and want the filament runout switch near the toolhead, but only if you are ready to manage CAN/USB toolhead wiring cleanly.", + ( + "This is why: a toolhead board lets the runout switch, extruder, hotend, fans, LEDs, and sensors terminate close to the toolhead instead of dragging more wires through the cable chain. " + "That usually improves serviceability and reduces harness bulk, but it adds firmware, bootloader, CAN/USB, termination, and spare-current details that have to be right." + ), + ( + "Lower-risk option: if the only new signal is one simple filament runout switch, wire it back to the M5P or another existing input and postpone the EBB42 until the motion system is stable. " + "Better upgrade path: use the EBB42 when you also want cleaner toolhead wiring, CAN/USB expansion, or future sensor/fan/LED growth." + ), + ( + "You should also consider: confirm the exact EBB42 revision, input voltage, CAN or USB plan, termination, cable strain relief, and Klipper pin names before buying or rewiring. " + "Do not let a toolboard upgrade hide a basic motion-system or harness problem." + ), + ] + ) + + def printer_profile_direct_answer(messages): query = latest_user_text(messages).strip() text = query.lower() @@ -9354,6 +11006,10 @@ def local_tool_catalog(): "check": "POST /api/tools/autonomy-supervisor", "description": "Check whether a draft answer needs help, web evidence, tools, artifacts, or a hard-boundary question before finalizing.", }, + "taskContractGate": { + "check": "POST /api/tools/task-contract", + "description": "Classify what done means for a request, list required proof/reject conditions, and optionally score a draft answer against the contract.", + }, "klipperConfigDiscovery": { "list": "GET /api/tools/klipper-configs?hint=qidi", "legacyList": "GET /api/tools/printer-configs?hint=ratrig", @@ -9394,6 +11050,7 @@ def build_local_tools_context(): "- To install a free allowlisted missing tool, call `POST http://127.0.0.1:8765/api/tools/install-free-tool` with JSON like `{\"tool\":\"jq\",\"reason\":\"parse printer API JSON\"}`.", "- To recover from a failure, call `POST http://127.0.0.1:8765/api/tools/recover` with the original messages, cwd, and error text. Use its recovery status before giving up.", "- To check whether a draft answer needs help before finalizing, call `POST http://127.0.0.1:8765/api/tools/autonomy-supervisor` with the messages, route, answerText, cwd, and webSearch.", + "- To define done before acting or to grade a draft against done, call `POST http://127.0.0.1:8765/api/tools/task-contract` with messages, route, optional answerText, and webSearch.", "- To classify a hard task or score a draft answer, call `POST http://127.0.0.1:8765/api/tools/analytical-core` with messages, route, answerText, and webSearch. Use it to fix wrong-objective, missing-constraint, no-decision, no-validation, and weak-evidence failures.", "- If the install response says `needsApproval`, ask Tinman before downloading. Do this for storage pressure, large installs, unknown tools, or anything not confirmed free.", "- After a successful install, retry the original task instead of stopping at `command not found`.", @@ -9572,10 +11229,11 @@ def candidate_matches_hint(candidate, hint): def discover_klipper_config_dirs(hint="", scan=False): + home = Path.home() known = [ - (Path.home() / "Downloads" / "ratrig_config", "current-klipper-config-ratrig"), - (Path.home() / "Documents" / "Codex", "user-codex-documents"), - (Path.home() / "Applications", "user-applications"), + (str(home / "Downloads" / "klipper_config"), "downloaded-klipper-config"), + (str(home / "Downloads" / "printer_config"), "downloaded-printer-config"), + (str(home / "Documents" / "Codex" / "printer_config"), "codex-printer-config"), ] candidates = [] seen = set() @@ -9587,9 +11245,9 @@ def discover_klipper_config_dirs(hint="", scan=False): if scan: scan_roots = [ - Path.home() / "Downloads", - Path.home() / "Documents" / "Codex", - Path.home() / "Applications", + home / "Downloads", + home / "Documents" / "Codex", + home / "Applications", ] for root in scan_roots: if not root.exists(): @@ -16055,12 +17713,200 @@ def add(name, status, detail=""): and "buildVolume" not in profile_answer_ambiguous ) add( - "tools:orca-profile-parameter-pull", - "pass" if ok else "fail", - "filament profile questions pull Orca/TinmanX1 parameters instead of CAD or machine specs", + "tools:orca-profile-parameter-pull", + "pass" if ok else "fail", + "filament profile questions pull Orca/TinmanX1 parameters instead of CAD or machine specs", + ) + except Exception as exc: + add("tools:orca-profile-parameter-pull", "fail", str(exc)) + + try: + creation_messages = [ + { + "role": "user", + "text": ( + "Can you create an Orca PETG filament profile for all printers all nozzle sizes? " + "Use the best practices and lessons learned from what we have done and industry." + ), + } + ] + LOCAL_ORCA_PROFILE_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="health-orca-profile-", dir=str(LOCAL_ORCA_PROFILE_OUTPUT_DIR)) as tmp_dir: + creation_result = stage_orca_profile_pack(creation_messages, target_path=tmp_dir) + creation_files_ok = ( + Path(creation_result.get("matrixPath", "")).exists() + and Path(creation_result.get("readmePath", "")).exists() + ) + creation_route = route_manager(creation_messages, requested_profile="manager", web_search="disabled") + ok = ( + is_orca_profile_creation_request(creation_messages) + and not is_filament_profile_pull_request(creation_messages) + and creation_route.get("projectId") == "tinmanx-slicer-research" + and creation_result.get("profileCount", 0) >= 12 + and creation_files_ok + ) + add( + "tools:orca-profile-creation-pack", + "pass" if ok else "fail", + f"{creation_result.get('profileCount', 0)} starter profiles generated for Orca import review", + ) + except Exception as exc: + add("tools:orca-profile-creation-pack", "fail", str(exc)) + + try: + nozzle_messages = [ + { + "role": "user", + "text": ( + "I am able to sync the filament type and color into the prepare tab. " + "I would still like to be able to see what nozzle is installed on the machine and what type in Orca. " + "What are my options at this point?" + ), + } + ] + nozzle_route = route_manager(nozzle_messages, requested_profile="manager", web_search="disabled") + nozzle_answer = orca_nozzle_visibility_direct_answer(nozzle_messages) + ok = ( + is_orca_nozzle_visibility_question(nozzle_messages) + and nozzle_route.get("projectId") == "tinmanx-slicer-research" + and "configured nozzle" in nozzle_answer.lower() + and "installed physical nozzle" in nozzle_answer.lower() + and "local machine inventory" in nozzle_answer.lower() + and "This is why:" in nozzle_answer + and "You should also consider:" in nozzle_answer + and "password" not in nozzle_answer.lower() + and "Fusion 360" not in nozzle_answer + ) + add( + "tools:orca-nozzle-visibility-options", + "pass" if ok else "fail", + "Orca nozzle visibility questions route to slicer workflow design, not live printer status", + ) + except Exception as exc: + add("tools:orca-nozzle-visibility-options", "fail", str(exc)) + + try: + workflow_cases = [ + ( + "orca-405", + [{"role": "user", "text": "When I try to print to my K2 Plus from Orca, I am getting HTTP 405 not allowed. How can I fix this?"}], + "tinmanx-slicer-research", + orca_http_405_direct_answer, + ("HTTP 405", "This is why:", "You should also consider:"), + ), + ( + "tinmanx-slice-stall", + [{"role": "user", "text": "in TinManX orca, the slice hits 80% then stalls. how can we fix this?"}], + "tinmanx-slicer-research", + tinmanx_slice_stall_direct_answer, + ("80% stall", "This is why:", "You should also consider:"), + ), + ( + "orca-device-tab", + [{"role": "user", "text": "on the centari carbon printers with open centauri firmware, in the devive tab of orca, is there any way to give me more control like in a standard klipper printer?"}], + "tinmanx-slicer-research", + orca_device_tab_control_direct_answer, + ("Device tab", "This is why:", "You should also consider:"), + ), + ( + "fusion-cam-stock-shoulder", + [{"role": "user", "text": "In Autodesk Fusion manufacture workspace I have a simple 2D contour and simulation gives Stock + Shoulder. How can I fix this?"}], + "cad-modeling-projects", + fusion_cam_stock_shoulder_direct_answer, + ("Stock + Shoulder", "This is why:", "You should also consider:"), + ), + ] + failed_workflows = [] + for name, case_messages, expected_project, answer_fn, terms in workflow_cases: + route = route_manager(case_messages, requested_profile="manager", web_search="disabled") + answer = answer_fn(case_messages) + score = analytical_answer_score(case_messages, route, answer, web_search="disabled") + if not ( + route.get("projectId") == expected_project + and answer + and all(term.lower() in answer.lower() for term in terms) + and int(score.get("score") or 0) >= 82 + ): + failed_workflows.append(name) + add( + "tools:slicer-cam-workflow-direct-answers", + "pass" if not failed_workflows else "fail", + "Orca workflow and Fusion CAM troubleshooting direct answers route correctly" + if not failed_workflows + else "failed cases: " + ", ".join(failed_workflows), + ) + except Exception as exc: + add("tools:slicer-cam-workflow-direct-answers", "fail", str(exc)) + + try: + direct_cases = [ + ( + "ai-print-failure", + [{"role": "user", "text": "What would be the best option for AI print failure monitoring?"}], + "general", + ("Best free/local", "This is why:", "You should also consider:"), + ), + ( + "aircraft-wood-defect", + [{"role": "user", "text": "Which defect is acceptable when choosing wood for aircraft structural repair? A Compression failure. B Splits. C Mineral streaks (not accompanied by decay)."}], + "cad-modeling-projects", + ("Pick C", "This is why:", "You should also consider:"), + ), + ( + "flightops-no-activity-charge", + [{"role": "user", "text": "Sometimes there is no flight activity for a customer for the month. If this is the case and there are services charges, we need to still show the charges. In N411GC for February there was a services charge for WB Air 2021 but he had no flights, this service fee was not posted to the report. Can you fix this?"}], + "flightops-tracker", + ("zero flight rows", "This is why:", "You should also consider:"), + ), + ( + "tinmanx-theme-branding", + [{"role": "user", "text": "can we change the colors on the app so I dont keep getting confused? Can you change the black on the orca to blue and the green to red and when it opens change the opening tile version from orca slicer ti TinmanX 1.0.0?"}], + "tinmanx-slicer-research", + ("source-level theme", "This is why:", "You should also consider:"), + ), + ( + "inverter-three-phase-input", + [{"role": "user", "text": "can you check and see if this inverter will directly take a 3 phase ac input? VEVOR 6400W 48V Hybrid Solar Inverter, 120V/240V Split Phase"}], + "energy-power-research", + ("do not feed", "split-phase", "This is why:", "You should also consider:"), + ), + ( + "aero-tool-install-path", + [{"role": "user", "text": "how do we get XFOIL, OpenVSP, SU2, and QBlade?"}], + "cad-modeling-projects", + ("install them in this order", "OpenVSP", "This is why:", "You should also consider:"), + ), + ( + "vague-failure-diagnostic", + [{"role": "user", "text": "On my cantauri Tinman, I believe I had a failure. Can you see what failure it was?"}], + "general", + ("most recent local app/log failure", "read-only", "This is why:", "You should also consider:"), + ), + ] + failed_direct = [] + for name, case_messages, expected_project, terms in direct_cases: + route = route_manager(case_messages, requested_profile="manager", web_search="disabled") + packet = general_direct_knowledge_answer(case_messages, route) + answer = (packet or {}).get("answer", "") + score = analytical_answer_score(case_messages, route, answer, web_search="disabled") + if not ( + route.get("projectId") == expected_project + and answer + and all(term.lower() in answer.lower() for term in terms) + and int(score.get("score") or 0) >= 82 + and "Recovery plan:" not in answer + and "Fusion 360" not in answer + ): + failed_direct.append(name) + add( + "tools:history-direct-answer-cases", + "pass" if not failed_direct else "fail", + "history-derived direct answers route and respond without cold fallback" + if not failed_direct + else "failed cases: " + ", ".join(failed_direct), ) except Exception as exc: - add("tools:orca-profile-parameter-pull", "fail", str(exc)) + add("tools:history-direct-answer-cases", "fail", str(exc)) try: profile_answer = printer_profile_direct_answer( @@ -16781,6 +18627,15 @@ def add(name, status, detail=""): except Exception as exc: add("response:examples-library", "fail", str(exc)) + try: + add( + "response:task-contract-gate", + "pass" if task_contract_gate_synthetic_check() else "fail", + "blocks fake hard-task completion, accepts honest blockers, and requires source/artifact proof", + ) + except Exception as exc: + add("response:task-contract-gate", "fail", str(exc)) + try: LOCAL_CAD_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix="health-package-", dir=str(LOCAL_CAD_OUTPUT_DIR)) as tmp_dir: @@ -16869,6 +18724,54 @@ def add(name, status, detail=""): except Exception as exc: add("analysis:hard-case-golden-tests", "fail", str(exc)) + try: + add( + "analysis:contract-gate-golden-tests", + "pass" if contract_gate_golden_tests_synthetic_check() else "fail", + f"{len(CONTRACT_GATE_GOLDEN_TEST_IDS)} contract-aware golden tests installed", + ) + except Exception as exc: + add("analysis:contract-gate-golden-tests", "fail", str(exc)) + + try: + domain_count = sum(len(info["questions"]) for info in DOMAIN_SAMPLE_QUESTION_GROUPS.values()) + add( + "analysis:domain-sample-golden-tests", + "pass" if domain_sample_golden_tests_synthetic_check() else "fail", + f"{domain_count} domain sample guardrails installed", + ) + except Exception as exc: + add("analysis:domain-sample-golden-tests", "fail", str(exc)) + + try: + fusion_orca_count = sum(len(info["questions"]) for info in FUSION_ORCA_SAMPLE_QUESTION_GROUPS.values()) + add( + "analysis:fusion-orca-sample-golden-tests", + "pass" if fusion_orca_sample_golden_tests_synthetic_check() else "fail", + f"{fusion_orca_count} Fusion 360 and Orca Slicer guardrails installed", + ) + except Exception as exc: + add("analysis:fusion-orca-sample-golden-tests", "fail", str(exc)) + + try: + manufacturing_count = len(load_manufacturing_question_bank()) + add( + "analysis:manufacturing-sample-golden-tests", + "pass" if manufacturing_sample_golden_tests_synthetic_check() else "fail", + f"{manufacturing_count} CAD/CNC manufacturing guardrails installed", + ) + except Exception as exc: + add("analysis:manufacturing-sample-golden-tests", "fail", str(exc)) + + try: + add( + "analysis:workflow-scenario-golden-tests", + "pass" if workflow_scenario_golden_tests_synthetic_check() else "fail", + "TinmanX1 Polymaker/steer/edit/self-repair/release scenario installed", + ) + except Exception as exc: + add("analysis:workflow-scenario-golden-tests", "fail", str(exc)) + health = ollama_health() add( "ollama:service", @@ -17092,6 +18995,9 @@ def normalize_direct_answer_shape(messages, route, text): replacements = ( (r"(?i)\bwhy it fits\s*:", "This is why:"), (r"(?i)\bwhy this fits\s*:", "This is why:"), + (r"(?i)\bwhy this happens\s*:", "This is why:"), + (r"(?i)\bwhy this matters\s*:", "This is why:"), + (r"(?i)\bwhy it matters\s*:", "This is why:"), (r"(?i)\bwhy it works\s*:", "This is why:"), (r"(?i)(?]+" + r"(?:_fusion360\.py|\.scad|\.stl|\.step|\.stp)", + raw, + flags=re.IGNORECASE, + ) + ) + + +def task_contract_gate(messages, route, answer, contract=None, deliverables=None, assumptions=None, web_search="live"): + text = str(answer or "") + lower = text.lower() + contract = contract or task_contract(messages, route or {}) + kind = contract.get("kind") or "General help" + deliverables = deliverables if deliverables is not None else extract_response_deliverables(text) + assumptions = assumptions if assumptions is not None else extract_assumption_ledger(messages, route or {}, text, contract) + blocker = answer_has_honest_blocker(text) + checks = [] + + def add(label, passed, detail="", severity="medium"): + checks.append( + { + "label": label, + "passed": bool(passed), + "detail": compact(detail, 200), + "severity": severity, + } + ) + + first = next((part.strip() for part in re.split(r"\n\s*\n", text) if part.strip()), "") + hard_kinds = { + "CAD/design deliverable", + "STL/CAD deliverable", + "Aero/CFD preflight", + "Mechanical/structural preflight", + "Engineering diagram", + "File/action", + "Code/config", + } + + add("Objective match", bool(first) and not first.lower().startswith(("recovery plan", "working notes")), "Answer starts with the requested outcome, not internal process.") + + if kind in {"Direct answer", "CAD reference", "Printer status"}: + add( + "Direct answer shape", + "this is why:" in lower and "you should also consider:" in lower, + "Direct questions need answer/why/consider unless the user asked for a terse command output.", + ) + + if kind in hard_kinds: + deliverable_or_blocker = bool(deliverables) or blocker + add( + "Required artifact/action proof", + deliverable_or_blocker, + "Hard tasks need clickable outputs or an explicit blocker before final.", + "hard", + ) + + if kind in {"CAD/design deliverable", "STL/CAD deliverable", "Aero/CFD preflight", "Mechanical/structural preflight", "Engineering diagram"}: + add( + "Assumptions and validation", + bool(assumptions) or blocker, + "Engineering tasks need assumptions, limits, validation, or a clear blocker.", + "hard", + ) + + if kind == "STL/CAD deliverable": + fake_geometry = answer_claims_generated_cad_geometry(text) and text_has_any( + lower, + ("did not find a readable stl", "attach the stl", "not attached"), + ) + add( + "No fake STL geometry", + not fake_geometry, + "Do not generate CAD geometry when the STL is missing.", + "hard", + ) + + if kind == "Research" and web_search == "live": + add( + "Source evidence", + answer_has_source_url(text), + "Research/current/spec/price answers need source URLs.", + "hard", + ) + + if kind == "CAD reference": + add( + "No artifact detour", + not answer_has_cad_artifact(text), + "Reference questions should not stage CAD files.", + "hard", + ) + + if kind == "Aero/CFD preflight": + add( + "No false CFD claim", + not answer_claims_solved_cfd(text) or bool(deliverables), + "Only claim solved CFD when solver output/report files are present.", + "hard", + ) + + if kind == "Mechanical/structural preflight": + has_strength_basis = text_has_any(lower, ("safety factor", "calculix", "load", "constraint", "stress", "deflection", "not final")) + add( + "Strength basis", + (not answer_claims_final_strength(text)) or has_strength_basis, + "Strength claims need load/material/safety-factor basis.", + "hard", + ) + + if kind == "Code/config": + add( + "Validation named", + text_has_any(lower, ("validated", "syntax", "quality-gate", "py_compile", "node --check", "klipper", "not run")), + "Code/config answers should name validation or why it could not run.", + "medium", + ) + + failed = [check for check in checks if not check.get("passed")] + hard_failed = [check for check in failed if check.get("severity") == "hard" or contract.get("hardGate")] + status = "block" if hard_failed else "review" if failed else "pass" + return { + "status": status, + "checks": checks, + "failed": failed, + "requiredProof": contract.get("requiredProof", []), + "rejectIf": contract.get("rejectIf", []), + } + + def response_scorecard(messages, route, answer, contract=None, deliverables=None, assumptions=None): text = str(answer or "").strip() lower = text.lower() contract = contract or task_contract(messages, route) deliverables = deliverables if deliverables is not None else extract_response_deliverables(text) assumptions = assumptions if assumptions is not None else extract_assumption_ledger(messages, route, text, contract) + gate = task_contract_gate(messages, route or {}, text, contract, deliverables, assumptions) first = next((part.strip() for part in re.split(r"\n\s*\n", text) if part.strip()), "") checks = [] def add(label, passed, detail=""): checks.append({"label": label, "passed": bool(passed), "detail": compact(detail, 180)}) + def add_gate(check): + checks.append( + { + "label": f"Contract: {check.get('label')}", + "passed": bool(check.get("passed")), + "detail": compact(check.get("detail", ""), 180), + } + ) + add( "Answer first", bool(first) and not first.lower().startswith(("working notes", "recovery plan", "fusion 360 script", "openscad model")), @@ -17393,8 +19542,11 @@ def add(label, passed, detail=""): analytical.get("score", 0) >= 82, "Answer should solve the actual problem, preserve constraints, use the right evidence/tools, and include a decision path.", ) + for check in gate.get("checks", []): + add_gate(check) score = int(round(100 * sum(1 for check in checks if check["passed"]) / max(1, len(checks)))) - return {"score": score, "status": "pass" if score >= 80 else "review", "checks": checks} + status = "review" if gate.get("status") == "block" else "pass" if score >= 80 and gate.get("status") == "pass" else "review" + return {"score": score, "status": status, "checks": checks, "contractGate": gate} def response_coach_answer(messages, route, answer): @@ -17416,6 +19568,8 @@ def response_package(messages, route, answer): assumptions = extract_assumption_ledger(messages, route or {}, coached, contract) scorecard = response_scorecard(messages, route or {}, coached, contract, deliverables, assumptions) analytical = analytical_answer_score(messages, route or {}, coached) + gate = scorecard.get("contractGate") or task_contract_gate(messages, route or {}, coached, contract, deliverables, assumptions) + contract = {**contract, "gateStatus": gate.get("status"), "gateFailures": gate.get("failed", [])[:6]} return { "text": coached, "taskContract": contract, @@ -17423,10 +19577,85 @@ def response_package(messages, route, answer): "deliverables": deliverables, "assumptions": assumptions, "scorecard": scorecard, + "contractGate": gate, "analyticalCore": analytical, } +def task_contract_gate_synthetic_check(): + cad_messages = [{"role": "user", "text": "Design a CPAP cooling duct in CAD for Fusion 360 with an 18mm inlet."}] + cad_route = {"projectId": "cad-modeling-projects", "engine": "local"} + fake_cad = task_contract_gate(cad_messages, cad_route, "I staged a CAD package for it.", web_search="disabled") + + reference_messages = [{"role": "user", "text": "what file type from fusion preserves component names?"}] + reference_route = {"projectId": "cad-modeling-projects", "engine": "local"} + wrong_reference = task_contract_gate( + reference_messages, + reference_route, + "Fusion 360 script: `/Users/example/generated/fusion360.py`\nOpenSCAD model: `/Users/example/generated/model.scad`", + web_search="disabled", + ) + + missing_stl_messages = [ + { + "role": "user", + "text": "missing-test.stl I need a CPAP duct designed from the attached STL with 1.5mm clearance.", + } + ] + missing_stl = task_contract_gate( + missing_stl_messages, + cad_route, + "I did not find a readable STL. Attach the STL file and I will inspect the mesh before generating duct geometry.", + web_search="disabled", + ) + + research_messages = [{"role": "user", "text": "Search the web for a 300 RPM wind generator under $500."}] + research_route = {"projectId": "energy-power-research", "engine": "local-research"} + sourceless = task_contract_gate( + research_messages, + research_route, + "Best pick is a 96V permanent magnet generator under $500.", + web_search="live", + ) + + good_cad = task_contract_gate( + cad_messages, + cad_route, + "Fusion 360 script: `/Users/example/generated/duct_fusion360.py`\n\nThis is why: the model preserves the 18mm inlet and keeps validation limits explicit.\n\nYou should also consider: no full CFD was run.", + deliverables=[{"path": "/Users/example/generated/duct_fusion360.py", "exists": True}], + assumptions=[{"kind": "Validation", "text": "No full CFD was run.", "status": "limited"}], + web_search="disabled", + ) + + return ( + fake_cad.get("status") == "block" + and wrong_reference.get("status") == "block" + and missing_stl.get("status") == "pass" + and sourceless.get("status") == "block" + and good_cad.get("status") == "pass" + ) + + +def format_contract_gate_blocker(messages, route, answer, contract, gate): + failures = gate.get("failed", []) if isinstance(gate, dict) else [] + missing = ", ".join(check.get("label", "required proof") for check in failures[:4]) or "required proof" + must_do = ", ".join((contract or {}).get("mustDo", [])[:3]) or "complete the required tool/evidence path" + proof = ", ".join((contract or {}).get("requiredProof", [])[:3]) or "verifiable output" + return "\n\n".join( + [ + "I’m not going to call this done yet, Tinman.", + ( + f"This is why: the task contract is `{(contract or {}).get('kind', 'unknown')}` and done means " + f"{(contract or {}).get('doneMeans', 'the requested work is complete')}. The draft is missing: {missing}." + ), + ( + f"You should also consider: the next pass needs to {must_do}. Required proof before final answer: {proof}. " + "I can keep working through that path instead of giving you a polished but incomplete answer." + ), + ] + ) + + def emit_assistant_answer(handler, messages, route, admin_topic, text, normalize=True): answer = normalize_direct_answer_shape(messages, route, text) if normalize else strip_thinking_markup(text) package = response_package(messages, route or {}, answer) @@ -17444,6 +19673,7 @@ def emit_assistant_answer(handler, messages, route, admin_topic, text, normalize "deliverables": package["deliverables"], "assumptions": package["assumptions"], "scorecard": package["scorecard"], + "contractGate": package["contractGate"], "analyticalCore": package["analyticalCore"], }, ) @@ -17501,6 +19731,13 @@ def supervise_answer_before_emit( except Exception as exc: if emit: emit(f"Analytical Core correction pass failed, keeping the best available answer: {compact(exc, 120)}") + contract = task_contract(messages, route or {}) + package = response_package(messages, route or {}, recovered or answer) + gate = package.get("contractGate") or {} + if gate.get("status") == "block": + if emit: + emit("Task Contract Gate blocked the draft from being marked complete.") + return format_contract_gate_blocker(messages, route or {}, recovered or answer, contract, gate) return recovered or answer @@ -18162,6 +20399,44 @@ def do_POST(self): record = record_quality_feedback(payload) improvement = record_improvement_from_feedback(record) golden_test = golden_test_from_feedback_improvement(record, improvement) + self_healing = None + if str(payload.get("rating") or "").lower() == "fix": + prompt = str(payload.get("prompt") or "") + messages = payload.get("messages") if isinstance(payload.get("messages"), list) else [] + if not messages and prompt: + messages = [{"role": "user", "text": prompt}] + route = payload.get("route") if isinstance(payload.get("route"), dict) else {} + self_healing = self_healing_supervise( + { + "trigger": "fix-this-feedback", + "messages": messages, + "answerText": payload.get("answer") or "", + "cwd": payload.get("cwd") or "", + "route": route, + "webSearch": payload.get("webSearch") or DEFAULT_WEB_SEARCH, + "autoRecover": True, + "autoInstall": True, + }, + record=True, + ) + if not self_healing.get("event", {}).get("patchQueued"): + patch_item = queue_self_patch_candidate( + self_healing_signature( + "fix-this-feedback", + messages, + answer_text=str(payload.get("answer") or payload.get("note") or ""), + ), + title="Fix-this feedback needs a code, routing, prompt, or test repair", + evidence=str(payload.get("note") or payload.get("answer") or "")[:900], + recommendation=( + "Diagnose why this answer missed Tinman's intent, patch the smallest responsible route/tool/prompt/UI path, " + "and keep the generated golden test as the regression guard." + ), + severity="medium", + next_action="Run the matching golden test after the repair, then package-health before publishing.", + ) + self_healing["event"]["patchQueued"] = patch_item + self_healing["queue"] = self_patch_queue_summary() except Exception as exc: self.send_json( { @@ -18182,6 +20457,8 @@ def do_POST(self): "goldenTests": golden_tests(), "qualityFeedback": quality_feedback_summary(), "improvementLab": improvement_lab_summary(), + "selfHealing": self_healing, + "selfHealingSummary": self_healing_summary(), "goldenTestSummary": golden_test_summary(), "admin": admin_summary(), } @@ -18361,6 +20638,42 @@ def do_POST(self): self.send_json({"ok": True, "profile": profile, "score": score, "route": route}) return + if parsed.path == "/api/tools/task-contract": + length = int(self.headers.get("Content-Length", "0") or "0") + try: + payload = json.loads(self.rfile.read(length).decode("utf-8") or "{}") + except json.JSONDecodeError: + self.send_error(400, "Invalid JSON") + return + messages = payload.get("messages") if isinstance(payload.get("messages"), list) else [] + if not messages: + prompt = str(payload.get("prompt") or payload.get("text") or "").strip() + if prompt: + messages = [{"role": "user", "text": prompt}] + web_search = safe_choice(payload.get("webSearch") or "live", WEB_SEARCH_LEVELS, "live") + route = payload.get("route") if isinstance(payload.get("route"), dict) else route_manager( + messages, + requested_profile="manager", + web_search=web_search, + ) + answer_text = payload.get("answerText") or payload.get("answer") or "" + contract = task_contract(messages, route) + gate = None + package = None + if answer_text: + package = response_package(messages, route, answer_text) + gate = package.get("contractGate") + self.send_json( + { + "ok": True, + "route": route, + "contract": contract, + "gate": gate, + "scorecard": (package or {}).get("scorecard"), + } + ) + return + if parsed.path == "/api/tools/quality-gate": length = int(self.headers.get("Content-Length", "0") or "0") try: @@ -18641,6 +20954,47 @@ def do_POST(self): if payload.get("testRun"): admin_topic = {**admin_topic, "testRun": True} + direct_knowledge = general_direct_knowledge_answer(messages, route) + if direct_knowledge: + self.send_response(200) + self.send_header("Content-Type", "application/x-ndjson; charset=utf-8") + self.send_header("Cache-Control", "no-cache") + self.send_header("X-Accel-Buffering", "no") + self.end_headers() + json_line( + self, + { + "type": "status", + "message": "starting", + "cwd": cwd, + "profile": profile, + "effectiveProfile": effective_profile, + "accessLevel": "local-knowledge", + "reasoningLevel": reasoning_level, + "webSearch": web_search, + "managerDepth": manager_depth, + "friendlinessLevel": friendliness_level, + "humorLevel": humor_level, + "mode": direct_knowledge.get("mode") or "direct-knowledge-answer", + "engine": "local-knowledge", + "model": "", + "freeOnlyRedirect": free_only_redirect, + "route": route, + "adminTopic": admin_topic, + }, + ) + json_line(self, {"type": "thought", "text": direct_knowledge.get("thought") or "Answering directly from local knowledge."}) + emit_assistant_answer( + self, + messages, + route, + admin_topic, + direct_knowledge.get("answer") or "", + normalize=False, + ) + json_line(self, {"type": "done", "returnCode": 0}) + return + if is_klipper_accel_rgb_tool_request(messages): self.send_response(200) self.send_header("Content-Type", "application/x-ndjson; charset=utf-8") @@ -18707,7 +21061,7 @@ def do_POST(self): json_line(self, {"type": "done", "returnCode": 0 if tool_result.get("ok") else 1}) return - direct_fusion_export_answer = fusion_component_export_direct_answer(messages) + direct_fusion_export_answer = fusion_component_export_direct_answer(messages) or fusion_cam_stock_shoulder_direct_answer(messages) if direct_fusion_export_answer: self.send_response(200) self.send_header("Content-Type", "application/x-ndjson; charset=utf-8") @@ -19274,11 +21628,17 @@ def do_POST(self): return direct_printing_expert_answer = ( - component_manual_direct_answer(messages) + toolboard_upgrade_decision_direct_answer(messages) + or component_manual_direct_answer(messages) or marlin_temperature_zero_diagnostic_answer(messages) + or orca_nozzle_visibility_direct_answer(messages) + or orca_http_405_direct_answer(messages) + or tinmanx_slice_stall_direct_answer(messages) + or orca_device_tab_control_direct_answer(messages) or temperature_tower_pressure_advance_direct_answer(messages) or orca_calibration_visual_direct_answer(messages) or temperature_tower_visual_direct_answer(messages) + or orca_profile_creation_direct_answer(messages) or filament_profile_parameters_direct_answer(messages) or filament_tuning_direct_answer(messages) or printer_profile_direct_answer(messages) diff --git a/Codex_CLI_UI/styles.css b/Codex_CLI_UI/styles.css index 32951d5..3977137 100644 --- a/Codex_CLI_UI/styles.css +++ b/Codex_CLI_UI/styles.css @@ -1794,6 +1794,35 @@ h1 { flex: 0 0 auto; } +.message-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + margin-top: 8px; +} + +.message-action-button { + border: 1px solid #cad8d0; + border-radius: 7px; + padding: 4px 8px; + background: #f7faf8; + color: var(--muted-strong); + font-size: 11px; + font-weight: 750; + cursor: pointer; +} + +.message-action-button:hover { + border-color: var(--accent); + color: var(--text); +} + +.message-action-button:disabled { + cursor: default; + opacity: 0.65; +} + .feedback-actions { display: flex; align-items: center; diff --git a/Codex_CLI_UI/tests/manufacturing_questions.json b/Codex_CLI_UI/tests/manufacturing_questions.json new file mode 100644 index 0000000..18b05b9 --- /dev/null +++ b/Codex_CLI_UI/tests/manufacturing_questions.json @@ -0,0 +1,107 @@ +{ + "version": 1, + "name": "Manufacturing Samples", + "description": "Public-safe CAD and CNC real-world questions from Tinman test bank.", + "questions": [ + {"id": 1, "category": "CAD", "question": "How do I design this part so it is easy to manufacture?"}, + {"id": 2, "category": "CAD", "question": "How do I choose datums for a part?"}, + {"id": 3, "category": "CAD", "question": "How do I build a parametric model that is easy to revise?"}, + {"id": 4, "category": "CAD", "question": "How do I avoid over-constraining a sketch?"}, + {"id": 5, "category": "CAD", "question": "How do I model parts for CNC machining?"}, + {"id": 6, "category": "CAD", "question": "How do I model parts for 3D printing?"}, + {"id": 7, "category": "CAD", "question": "How do I add proper fillets and chamfers?"}, + {"id": 8, "category": "CAD", "question": "How do I design holes for bolts, pins, and inserts?"}, + {"id": 9, "category": "CAD", "question": "How do I create a clean assembly structure?"}, + {"id": 10, "category": "CAD", "question": "How do I prevent parts from interfering in an assembly?"}, + {"id": 11, "category": "CAD", "question": "How do I design parts with proper clearance?"}, + {"id": 12, "category": "CAD", "question": "How do I apply GD&T to a drawing?"}, + {"id": 13, "category": "CAD", "question": "How do I dimension a drawing without overdefining it?"}, + {"id": 14, "category": "CAD", "question": "How do I choose tolerances for mating parts?"}, + {"id": 15, "category": "CAD", "question": "How do I create a drawing from a 3D model?"}, + {"id": 16, "category": "CAD", "question": "How do I model threads correctly?"}, + {"id": 17, "category": "CAD", "question": "How do I design press-fit features?"}, + {"id": 18, "category": "CAD", "question": "How do I design slip-fit features?"}, + {"id": 19, "category": "CAD", "question": "How do I model bearings, bushings, and shafts?"}, + {"id": 20, "category": "CAD", "question": "How do I design a part for sheet metal bending?"}, + {"id": 21, "category": "CAD", "question": "How do I calculate bend allowance or K-factor?"}, + {"id": 22, "category": "CAD", "question": "How do I design ribs and gussets for stiffness?"}, + {"id": 23, "category": "CAD", "question": "How do I reduce weight while keeping strength?"}, + {"id": 24, "category": "CAD", "question": "How do I prepare a CAD model for FEA?"}, + {"id": 25, "category": "CAD", "question": "How do I simplify geometry for simulation?"}, + {"id": 26, "category": "CAD", "question": "How do I model weldments or tube frames?"}, + {"id": 27, "category": "CAD", "question": "How do I design brackets for load paths?"}, + {"id": 28, "category": "CAD", "question": "How do I design a housing or enclosure?"}, + {"id": 29, "category": "CAD", "question": "How do I design bosses and standoffs?"}, + {"id": 30, "category": "CAD", "question": "How do I split a part into manufacturable pieces?"}, + {"id": 31, "category": "CAD", "question": "How do I use reference geometry effectively?"}, + {"id": 32, "category": "CAD", "question": "How do I create robust mate constraints?"}, + {"id": 33, "category": "CAD", "question": "How do I manage design intent in a model?"}, + {"id": 34, "category": "CAD", "question": "How do I prevent rebuild errors in CAD?"}, + {"id": 35, "category": "CAD", "question": "How do I model organic or curved surfaces?"}, + {"id": 36, "category": "CAD", "question": "How do I create lofts, sweeps, and boundary surfaces?"}, + {"id": 37, "category": "CAD", "question": "How do I reverse-engineer a physical part into CAD?"}, + {"id": 38, "category": "CAD", "question": "How do I design around standard hardware?"}, + {"id": 39, "category": "CAD", "question": "How do I model O-ring grooves and seals?"}, + {"id": 40, "category": "CAD", "question": "How do I design parts that are easy to inspect?"}, + {"id": 41, "category": "CAD", "question": "How do I create exploded views and assembly drawings?"}, + {"id": 42, "category": "CAD", "question": "How do I organize part files and revisions?"}, + {"id": 43, "category": "CAD", "question": "How do I convert mesh files into usable CAD?"}, + {"id": 44, "category": "CAD", "question": "How do I export STEP, STL, DXF, or IGES correctly?"}, + {"id": 45, "category": "CAD", "question": "How do I design for casting, molding, or forming?"}, + {"id": 46, "category": "CAD", "question": "How do I add draft angles to a molded part?"}, + {"id": 47, "category": "CAD", "question": "How do I check wall thickness in CAD?"}, + {"id": 48, "category": "CAD", "question": "How do I run interference or clearance checks?"}, + {"id": 49, "category": "CAD", "question": "How do I create a technical drawing package?"}, + {"id": 50, "category": "CAD", "question": "How do I make a CAD model that another engineer can understand?"}, + {"id": 51, "category": "CNC Machining", "question": "How do I choose the right tool for this material and feature?"}, + {"id": 52, "category": "CNC Machining", "question": "How do I calculate feeds and speeds?"}, + {"id": 53, "category": "CNC Machining", "question": "How do I reduce chatter during milling?"}, + {"id": 54, "category": "CNC Machining", "question": "How do I choose between climb and conventional milling?"}, + {"id": 55, "category": "CNC Machining", "question": "How do I fixture a part securely?"}, + {"id": 56, "category": "CNC Machining", "question": "How do I decide the best machining setup order?"}, + {"id": 57, "category": "CNC Machining", "question": "How do I machine thin walls without distortion?"}, + {"id": 58, "category": "CNC Machining", "question": "How do I hold tight tolerances on a milled part?"}, + {"id": 59, "category": "CNC Machining", "question": "How do I choose roughing versus finishing strategies?"}, + {"id": 60, "category": "CNC Machining", "question": "How do I select stepdown and stepover values?"}, + {"id": 61, "category": "CNC Machining", "question": "How do I machine deep pockets effectively?"}, + {"id": 62, "category": "CNC Machining", "question": "How do I avoid tool deflection?"}, + {"id": 63, "category": "CNC Machining", "question": "How do I machine sharp internal corners?"}, + {"id": 64, "category": "CNC Machining", "question": "How do I choose corner radii for milled pockets?"}, + {"id": 65, "category": "CNC Machining", "question": "How do I drill accurate holes?"}, + {"id": 66, "category": "CNC Machining", "question": "How do I ream a hole to final size?"}, + {"id": 67, "category": "CNC Machining", "question": "How do I tap threads in CNC machining?"}, + {"id": 68, "category": "CNC Machining", "question": "How do I thread mill a hole?"}, + {"id": 69, "category": "CNC Machining", "question": "How do I machine aluminum cleanly?"}, + {"id": 70, "category": "CNC Machining", "question": "How do I machine steel without burning tools?"}, + {"id": 71, "category": "CNC Machining", "question": "How do I machine stainless steel?"}, + {"id": 72, "category": "CNC Machining", "question": "How do I machine plastics without melting them?"}, + {"id": 73, "category": "CNC Machining", "question": "How do I machine composites safely and accurately?"}, + {"id": 74, "category": "CNC Machining", "question": "How do I choose coolant, mist, or dry machining?"}, + {"id": 75, "category": "CNC Machining", "question": "How do I improve surface finish?"}, + {"id": 76, "category": "CNC Machining", "question": "How do I avoid burrs during machining?"}, + {"id": 77, "category": "CNC Machining", "question": "How do I deburr parts without damaging edges?"}, + {"id": 78, "category": "CNC Machining", "question": "How do I choose the right end mill flute count?"}, + {"id": 79, "category": "CNC Machining", "question": "How do I choose coated versus uncoated tools?"}, + {"id": 80, "category": "CNC Machining", "question": "How do I set tool length offsets?"}, + {"id": 81, "category": "CNC Machining", "question": "How do I set work coordinate systems?"}, + {"id": 82, "category": "CNC Machining", "question": "How do I use probing to locate a part?"}, + {"id": 83, "category": "CNC Machining", "question": "How do I verify a setup before cutting?"}, + {"id": 84, "category": "CNC Machining", "question": "How do I avoid crashes in CAM toolpaths?"}, + {"id": 85, "category": "CNC Machining", "question": "How do I simulate toolpaths properly?"}, + {"id": 86, "category": "CNC Machining", "question": "How do I machine 3D contours?"}, + {"id": 87, "category": "CNC Machining", "question": "How do I choose ball end mills for contouring?"}, + {"id": 88, "category": "CNC Machining", "question": "How do I machine slots accurately?"}, + {"id": 89, "category": "CNC Machining", "question": "How do I machine keyways or grooves?"}, + {"id": 90, "category": "CNC Machining", "question": "How do I machine multiple parts repeatably?"}, + {"id": 91, "category": "CNC Machining", "question": "How do I use soft jaws?"}, + {"id": 92, "category": "CNC Machining", "question": "How do I machine a part on both sides accurately?"}, + {"id": 93, "category": "CNC Machining", "question": "How do I indicate or tram a vise?"}, + {"id": 94, "category": "CNC Machining", "question": "How do I square raw stock before machining?"}, + {"id": 95, "category": "CNC Machining", "question": "How do I leave stock for finishing passes?"}, + {"id": 96, "category": "CNC Machining", "question": "How do I prevent workpiece vibration?"}, + {"id": 97, "category": "CNC Machining", "question": "How do I machine very small features?"}, + {"id": 98, "category": "CNC Machining", "question": "How do I choose between 3-axis, 4-axis, and 5-axis machining?"}, + {"id": 99, "category": "CNC Machining", "question": "How do I inspect machined parts with calipers, micrometers, or CMM?"}, + {"id": 100, "category": "CNC Machining", "question": "How do I troubleshoot dimensional errors after machining?"} + ] +} diff --git a/checks/verify_release.sh b/checks/verify_release.sh index 5c4571d..40a6854 100755 --- a/checks/verify_release.sh +++ b/checks/verify_release.sh @@ -8,7 +8,11 @@ bash -n "$ROOT/install/Install Codex CLI UI.command" bash -n "$ROOT/install/Uninstall Codex CLI UI.command" echo "Checking Python" -/usr/bin/python3 -m py_compile "$ROOT/Codex_CLI_UI/server.py" "$ROOT/Codex_CLI_UI/import_codex_history.py" +/usr/bin/python3 -m py_compile \ + "$ROOT/Codex_CLI_UI/server.py" \ + "$ROOT/Codex_CLI_UI/import_codex_history.py" \ + "$ROOT/Codex_CLI_UI/harvest_history_golden_tests.py" \ + "$ROOT/Codex_CLI_UI/run_golden_batch.py" if [ -x /Applications/Codex.app/Contents/Resources/cua_node/bin/node ]; then /Applications/Codex.app/Contents/Resources/cua_node/bin/node --check "$ROOT/Codex_CLI_UI/app.js" @@ -24,7 +28,7 @@ if find "$ROOT" -name .git -prune -o -type f \( -name 'codex_history_index.jsonl exit 1 fi -if rg -n --hidden --glob '!.git' --glob '!.git/**' --glob '!**/checks/verify_release.sh' --glob '!release/*.dmg' --glob '!*.icns' --glob '!*.png' \ +if rg -n --hidden --glob '!.git' --glob '!.git/**' --glob '!**/checks/verify_release.sh' --glob '!release/*.dmg' --glob '!release/*.zip' --glob '!*.icns' --glob '!*.png' \ 'williamtinney|/Users/williamtinney|192\.168\.|makersvpn|gho_|QIDI@|Flightops_Tracker' "$ROOT"; then echo "Privacy scan found a blocked pattern" exit 1 diff --git a/release/Codex_CLI_UI_Mac_Public_Bundle_2026-07-03.zip b/release/Codex_CLI_UI_Mac_Public_Bundle_2026-07-03.zip new file mode 100644 index 0000000..6c5291e Binary files /dev/null and b/release/Codex_CLI_UI_Mac_Public_Bundle_2026-07-03.zip differ diff --git a/release/RELEASE_NOTES.md b/release/RELEASE_NOTES.md index a854e30..984714b 100644 --- a/release/RELEASE_NOTES.md +++ b/release/RELEASE_NOTES.md @@ -1,24 +1,38 @@ -# Codex CLI UI for Mac v2026.07.14 +# Codex CLI UI for Mac v2026.07.15 -3D printing expert-pack release. +World-class response-quality and manufacturing-test release. ## Download Download one file: ```text -Codex_CLI_UI_Mac_v2026.07.14.dmg +Codex_CLI_UI_Mac_Public_Bundle_2026-07-03.zip ``` -Open the DMG and double-click `Install Codex CLI UI.command`. +Unzip it, then open the included DMG or double-click `install/Install Codex CLI UI.command`. ## Fixed +- Task Contract Gate now blocks hard-task answers from being marked complete unless required proof, artifacts, source evidence, or an honest blocker are present. +- Fusion 360 user-parameter questions no longer get mistaken for Orca pressure-advance or slicer-profile pulls. +- Conceptual Orca filament/profile questions now answer the workflow directly instead of dumping unrelated local profile data. - Printer/material/component knowledge questions now answer directly instead of falling into generic CAD, Moonraker, or printer-status fallbacks. - Package health now catches 3D-printing expert-pack, Orca tuning, and printer-profile regressions before release. +- CAD and CNC knowledge questions now route to manufacturing expertise instead of generic CAD artifact staging. +- Complex Codex CLI UI/TinmanX1 requests now route to the local-agent workflow lane instead of getting pulled sideways by filament keywords. +- `Fix this` feedback now creates a lesson, a golden regression test, and a self-healing repair candidate with evidence. ## Added +- Public 50-question Fusion & Orca Samples golden-test bank covering Fusion 360 CAD/CAM workflows, OrcaSlicer Codex app/preset repair, Orca profile workflow, and Orca filament calibration. +- Task Contract Gate API and scorecard fields for defining done, required proof, reject conditions, and gate status on responses. +- Public 100-question Manufacturing Samples golden-test bank: 50 CAD questions and 50 CNC machining questions. +- TinmanX1/Polymaker/Fiberon workflow scenario covering test-bank import, Steer/Edit UI, self-healing, GitHub release, and zip packaging behavior. +- `Edit question` message action for user prompts. It reloads the question into the composer and reruns from that point. +- `Steer` message action for assistant answers. It preloads a correction prompt so Tinman can redirect the answer without starting over. +- Package-health checks for manufacturing-sample tests and workflow-scenario tests. +- README commands for running manufacturing samples and the workflow scenario through the live `/api/run` test bench. - 3D Printing Expert Pack with printer profiles for Bambu H2D/X1C, Creality K2 Plus, Qidi Plus 4, Snapmaker U1, Rat Rig V-Core 4.1 IDEX Klipper, Sovol SV08 Max, and ELEGOO Centauri Carbon. - Filament/material library for PLA, PETG, PCTG, ABS, ASA, PA, PA-CF, PET-CF, PC, and TPU with drying, use-case, strength, and caution notes. - OrcaSlicer tuning coach using the practical calibration order: temperature tower, flow pass 1, flow pass 2, pressure advance, max volumetric speed, retraction/stringing, then VFA/speed. @@ -47,6 +61,8 @@ Open the DMG and double-click `Install Codex CLI UI.command`. - Fast, Careful, Coder, Review, Manager, and Local Research modes - Web Access, access level, reasoning, friendliness, and humor controls - Admin Improvement Lab, Golden Test Generator, Test Bench history, and package health checks +- Public CAD/CNC manufacturing regression fixture +- Steer/Edit chat controls and self-healing Fix-this queue - Tool Recovery Engine and Capability Manager - Autonomy Supervisor help-needed checker - Klipper config discovery and macro-staging helpers diff --git a/release/SHA256SUMS.txt b/release/SHA256SUMS.txt index a8a19f2..aa6761e 100644 --- a/release/SHA256SUMS.txt +++ b/release/SHA256SUMS.txt @@ -1,14 +1,15 @@ -044dd09c134c607308cfec4c09458f01177fc3893cc8e1562d6fa6a63ba21ce9 Codex_CLI_UI_Mac_v2026.07.01.1.dmg -119cce84c571f82c4875020c406ee983331488e4fc6c4931c69e102bbfa49b28 Codex_CLI_UI_Mac_v2026.07.02.dmg -25ff282fe8f858b267e7044429ef67a5024cfb610fdabea02fc1eead9f5c9b8c Codex_CLI_UI_Mac_v2026.07.03.dmg -c8ca12fedbf01442d952d5ee2c0841965511341d530716c949b8f5cbfa4be82d Codex_CLI_UI_Mac_v2026.07.04.dmg -f4c3cf274bb55530192e892d36915e231f044bc204670c82ad73bc35a8beee34 Codex_CLI_UI_Mac_v2026.07.05.dmg -aed664708912f9753f197e92fc94e67016f9c5476517cc72988ea98bf5a07840 Codex_CLI_UI_Mac_v2026.07.06.dmg -d36050080410a59e43518ab3da8a7d19230011b27f1f45405c0309138e030800 Codex_CLI_UI_Mac_v2026.07.07.dmg -1de71f85f16f9e623b6f6091055537f116178e3f72b568e09d644a20788b6b07 Codex_CLI_UI_Mac_v2026.07.08.dmg -338ba4f9dab42995153065231e9ebae23a14a2aae5770b7656809e071c77326b Codex_CLI_UI_Mac_v2026.07.09.dmg -920fa5c832bffe1d1bf9b8f5c40d51cbf3c758eff9440295e419657ed788803b Codex_CLI_UI_Mac_v2026.07.10.dmg -b6fb7ffa0e0047175835a601b30181127da433432d4697680d24db184f39f0d2 Codex_CLI_UI_Mac_v2026.07.11.dmg -68c3f4221bc02dc6ca30fa3eb501b7f8b47d82e5a73bbbad9ad1311234745f66 Codex_CLI_UI_Mac_v2026.07.12.dmg -a7b23298017561911711c909e777935c64d5d6a04d87703b9b49975933c035b9 Codex_CLI_UI_Mac_v2026.07.13.dmg -7c005e8ff7a53cacdad206a9172b26a4903bb0d635409be63e5612c227a6d128 Codex_CLI_UI_Mac_v2026.07.14.dmg +044dd09c134c607308cfec4c09458f01177fc3893cc8e1562d6fa6a63ba21ce9 release/Codex_CLI_UI_Mac_v2026.07.01.1.dmg +119cce84c571f82c4875020c406ee983331488e4fc6c4931c69e102bbfa49b28 release/Codex_CLI_UI_Mac_v2026.07.02.dmg +25ff282fe8f858b267e7044429ef67a5024cfb610fdabea02fc1eead9f5c9b8c release/Codex_CLI_UI_Mac_v2026.07.03.dmg +c8ca12fedbf01442d952d5ee2c0841965511341d530716c949b8f5cbfa4be82d release/Codex_CLI_UI_Mac_v2026.07.04.dmg +f4c3cf274bb55530192e892d36915e231f044bc204670c82ad73bc35a8beee34 release/Codex_CLI_UI_Mac_v2026.07.05.dmg +aed664708912f9753f197e92fc94e67016f9c5476517cc72988ea98bf5a07840 release/Codex_CLI_UI_Mac_v2026.07.06.dmg +d36050080410a59e43518ab3da8a7d19230011b27f1f45405c0309138e030800 release/Codex_CLI_UI_Mac_v2026.07.07.dmg +1de71f85f16f9e623b6f6091055537f116178e3f72b568e09d644a20788b6b07 release/Codex_CLI_UI_Mac_v2026.07.08.dmg +338ba4f9dab42995153065231e9ebae23a14a2aae5770b7656809e071c77326b release/Codex_CLI_UI_Mac_v2026.07.09.dmg +920fa5c832bffe1d1bf9b8f5c40d51cbf3c758eff9440295e419657ed788803b release/Codex_CLI_UI_Mac_v2026.07.10.dmg +b6fb7ffa0e0047175835a601b30181127da433432d4697680d24db184f39f0d2 release/Codex_CLI_UI_Mac_v2026.07.11.dmg +68c3f4221bc02dc6ca30fa3eb501b7f8b47d82e5a73bbbad9ad1311234745f66 release/Codex_CLI_UI_Mac_v2026.07.12.dmg +a7b23298017561911711c909e777935c64d5d6a04d87703b9b49975933c035b9 release/Codex_CLI_UI_Mac_v2026.07.13.dmg +7c005e8ff7a53cacdad206a9172b26a4903bb0d635409be63e5612c227a6d128 release/Codex_CLI_UI_Mac_v2026.07.14.dmg +b61374f5b824eb6e348042fc2c8f19404113753a821d052e4cf703cde7e4cbba release/Codex_CLI_UI_Mac_Public_Bundle_2026-07-03.zip