Clinical Trial Chief Advisor
Published Application
| Item | Value |
|---|---|
| Share link | https://ct-advisor.app.workbuddy.host/ |
| appId | wbapp_FBxILYuXi6yMolY8CsYzA7 |
| Payload | workbench/publish/ — in-skill, workspace-independent; deploy metadata in workbench/app.config.json (Python HTTP service) |
Re-publish rule: always overwrite with the existing
appId— the link must stayhttps://ct-advisor.app.workbuddy.host/; nevercreateNewApp. After deploy, assert the returnedshareLinkequals the expected URL (ct-base §13.5 dirty-binding red line). Last republished 2026-09-26.
Language
- English guide → README.md · 中文指南 → README_zh-CN.md
- This SKILL.md body is English-only, agent-facing. Bilingual walkthroughs live in the two READMEs; the answer language follows the user's question language.
🔴 HIGHEST PRIORITY: entry.py is the ONLY permitted action
The local LLM is FORBIDDEN from making any decisions in this skill. Its ONE AND ONLY allowed action is:
python scripts/entry.py --q "<user question>" [--attach "<path>"]and then pipe the stdout to the user verbatim.
HARD GATE — the following are absolutely PROHIBITED for the local LLM:
- ❌ NEVER read, search, or reference any local knowledge base (the
knowledge/directory has been removed) - ❌ NEVER perform any local network retrieval or web search (
references/search-sites.mdhas been removed) - ❌ NEVER answer the question from the LLM's own knowledge — all domain expertise lives on the Coze side
- ❌ NEVER bypass
entry.pyby callingroute.py,refine_answer.py,orchestrate.py, or any other script directly - ❌ NEVER rewrite, rephrase, reorder, summarize, or "polish" the answer text obtained by base64-decoding the payload between
<<<CT_ANSWER_B64_START>>>and<<<CT_ANSWER_B64_END>>> - ❌ NEVER append a summary, lead-in, closing remark, or "key takeaways" before or after the decoded answer
- ❌ NEVER strip or alter the
checksum:line that follows<<<CT_ANSWER_B64_END>>> - ❌ NEVER translate or align language (the code already handles that)
- ❌ NEVER inject process narration ("Step 2", "Coze returned", "assembling payload")
- ❌ NEVER decide whether to forward to Coze — ALL questions are forwarded (entry.py handles routing internally)
Why this rule exists: The local LLM's "helpful" instincts (answering from its own knowledge, pre-reading files, post-formatting answers) have repeatedly violated the pipeline contract, destroyed Coze-delivered content formatting, altered numeric precision, injected unsolicited content, and bypassed the cloud analysis entirely. This rule eliminates ALL local LLM decision space.
How to use this skill
With a question only:
python scripts/entry.py --q "In phase III NSCLC patients, comparing pembrolizumab vs chemotherapy, how is the sample size for the primary endpoint OS estimated?"
With an attachment:
python scripts/entry.py --q "Is this document related to medicine?" --attach "/path/to/file.docx"
That's it. The stdout is the final answer — pipe it to the user verbatim.
What entry.py does internally (all code, zero LLM) — the canonical pipeline
STEP 1 Attachment gate (if --attach) — BOTH paths ship a FILE via doc_context
├─ < 5 MB → doc_memory.build_file_payload(allow_upload=True): upload ORIGINAL file to Coze /upload_file, ship as `doc_context` (mode=file_id; base64-forward fallback on failure)
└─ > 5 MB → convert locally (office_to_md.py for OOXML / direct read for txt-like), upload the resulting **.md** through the SAME doc_context channel + ℹ️ notice that cloud answers from the md version
STEP 2 Vague gate (deterministic regex, scripts/route.py — binary: vague | forwarded, the ONLY local gate): vague → clarify_loop.py (≤3 rounds) → questions to user, stop; forwarded → continue (Coze re-judges difficulty with its own LLM; timeout tier via route.timeout_tier)
STEP 3 Forward to Coze — orchestrate.run_orchestrate(payload): parallel Coze /run fire + local ct-skill prefetch → merge
STEP 4 Delegate stitch — if Coze returns <<<CT_TOOL_DELEGATE>>>, entry.py auto-runs refine_answer.py --card-inline (code, no LLM)
STEP 5 Output — stdout carries a base64 ASCII-safe payload in <<<CT_ANSWER_B64_START/END>>> + sha256 checksum
LLM-forbidden zone: every STEP above is executed by code. The local LLM never picks the channel, never judges difficulty, never decides forwarding.
Output format:
<<<CT_ANSWER_B64_START>>>
{base64 of the LF-normalized answer}
<<<CT_ANSWER_B64_END>>>
checksum: {sha256[:16] of the LF-normalized answer}
🔴 base64 ASCII-safe payload (v1.3.9): the answer is base64-encoded before output — the delimiters, payload and checksum line are pure ASCII (a subset of every console code page, incl. cp936/GBK). PowerShell 5.1 decoding captured stdout with the wrong code page therefore cannot corrupt the payload: this structurally closes the "GBK mojibake → byte-level checksum false-alarm → agent misreads it as encoding loss/tampering → pointless Coze re-run" loop. Decode FIRST, then verify (see the check code below). The checksum is still computed over the LF-normalized answer text (CRLF-tolerance retained). A plain-text
<<<CT_ANSWER_START>>>(non-b64) block is legacy/debug output only — if you ever see one, apply the same decode-then-verify discipline but treat mojibake as possible.
🔴 sha256 Anti-tamper Check (HARD GATE):
After receiving the stdout from entry.py, the local LLM MUST verify the checksum before passing the answer to the user:
import base64, hashlib, re
stdout = "...(entry.py output)..."
m = re.search(r'<<<CT_ANSWER_B64_START>>>\r?\n(.*?)\r?\n<<<CT_ANSWER_B64_END>>>\r?\nchecksum: (.+)', stdout, re.DOTALL)
if m:
# Decode the base64 payload FIRST (v1.3.9): the b64 charset is pure ASCII,
# so the payload survives ANY console code page (cp936 included) byte-for-byte.
# Strip ALL whitespace before decoding (2026-10-15 audit): PowerShell
# `Out-String` wraps long payloads (>80 display cols — a 1000-char answer is
# ~1.3k+ b64 chars) with mid-payload newlines; Python's b64decode tolerates
# them but strict decoders (e.g. .NET FromBase64String) do not. Then hash the
# LF-normalized answer (CRLF-tolerance retained).
answer = base64.b64decode(re.sub(r"\s+", "", m.group(1))).decode("utf-8")
expected_checksum = m.group(2).strip()
actual_checksum = hashlib.sha256(answer.replace("\r\n", "\n").encode("utf-8")).hexdigest()[:16]
if actual_checksum != expected_checksum:
# DO NOT blindly re-run — work the "Checksum-mismatch decision table" below first.
print("⚠️ answer integrity check failed, please retry.")
else:
# OK — pass answer verbatim
print(answer)
If the checksum does not match, the local LLM MUST NOT deliver the answer to the user. This prevents any silent tampering.
🔴 Capture stdout BYTE-FAITHFULLY (Windows PowerShell · 2026-10-15): since v1.3.9 the answer payload is base64 (pure ASCII), so the checksum-relevant bytes survive any console code page — verified end-to-end on a Chinese Windows with the default
gb2312/cp936[Console]::OutputEncoding(payload intact, checksum matched). Two residual notes: (1) setting[Console]::OutputEncoding = [System.Text.Encoding]::UTF8before invokingentry.pyis still recommended — it keeps stderr diagnostics (Chinese log lines) readable, but is no longer load-bearing for the gate; (2) if you ever capture a legacy plain-text<<<CT_ANSWER_START>>>block (debug output), GBK mojibake IS possible again — decode-then-verify per the decision table. In Git Bash /cmd /cwith file redirection / WSL the byte stream is faithful — no action needed. Do NOT "fix" encoding by writing stdout to a temp file (see the L3 discipline below).
🔴 Checksum-mismatch decision table (HARD GATE · 2026-10-15): a mismatch is NOT a licence to re-run. Work the table IN ORDER and stop at the first hit:
# Check Action 1 Re-hash after CRLF normalization ( .replace("\r\n", "\n")— see code above)Match → deliver verbatim. Done. 1b b64decoderaised (e.g. strict decoder met mid-payload newlines from PowerShellOut-Stringline-wrapping of a long payload)Strip ALL whitespace from the b64 field ( re.sub(r"\s+", "", m.group(1))) and decode again — the verify code above already does this. Only if it still fails treat it as a corrupted capture → step 2. Never re-run Coze for this.1c stdout ends with a truncation marker (e.g. …[输出截断]) or the trailingchecksum:line is missing entirelyThe capture tool truncated the echo (PowerShell tool ≈20k chars; the absolute worst case — coze 1000-char answer + 4000-char skill result both maxed — is ~20,150 b64 chars, verified intact on Bash). Switch to the Bash / Git Bash tool and re-capture — do NOT re-run Coze. 2 Is the captured answer visibly mojibake (CJK rendered as 鏈熶复…-style garbage) or truncated?With the v1.3.9 base64 payload this should no longer happen for the answer block — if it does, you are looking at legacy/debug plain-text output or a corrupted capture pipeline. The corrupted text is NOT repairable in place — cp936 mis-decodes some UTF-8 byte pairs as illegal (e.g. 0x9A) and replaces them with U+FFFD, permanently destroying the original bytes (both deterministic byte-reversal and any LLM rewrite were tested and cannot restore them). Fix the capture (set[Console]::OutputEncodingto UTF-8, or re-capture in Git Bash reading the FULL stdout into a variable) and re-send Coze at most once, stating the reason to the user. Cost note: on the serial path the re-run hits the local dedup short-circuit (30-min window, ≥0.90 similarity) and replays the clean cached answer with no new Coze call — stderr showsdedup short-circuit: replayed local cached answer. Even a genuine re-send is cheap: the Coze backend has a 4-gate server-side cache (cross-request hit, verified).3 Regex found NO <<<CT_ANSWER_B64_START>>>block at all (stdout structure destroyed, or empty stdout)First rule out a real endpoint fault — check stderr for [ct-advisor][ERR]and runcheck_coze.py(token / dead proxy / network / unreachable). If the endpoint is healthy this is still a capture-layer failure → same repair as step 2, re-send ≤1. If the endpoint itself is down, report the diagnosis to the user and stop — do not re-send into a dead endpoint.4 Still mismatched, text looks clean Treat as potential tampering: do NOT deliver; do NOT loop re-runs; report expected vs actual checksums to the user and stop. Re-running without evidence from steps 2–3 is a protocol violation (it caused the observed incident: the same question fired 2–3 times, a single Q&A dragging to minutes).
🔴 No host-model fallback (HARD GATE · 2026-10-15): ct-advisor is coze-only by design — there is no local knowledge base or database, so a host-model direct answer on a clinical question has no domain substrate and is forbidden as a fallback (a bare host-model answer was evaluated and rejected: quality cannot be guaranteed without the cloud knowledge base and tool chain). The same tiering rule means attachment /
doc_contextcontent must never leave the Coze channel. The ONLY sanctioned recovery for a broken capture is: repair the capture → re-send once (≤1, dedup-cached in the common case). Everything else stops and reports.
🔴 Short-circuit output is also GATE-wrapped (2026-10-15): the dedup short-circuit path emits its cached answer through the same
<<<CT_ANSWER_B64_START/END>>> + checksumwrapper as a fresh Coze call (stderr marksdedup short-circuit). If your regex matches the block, decode-and-verify it like any other answer — a cached answer is a legitimate delivery, not an error.
🔴 Single-run discipline (performance · as mandatory as the HARD GATE):
entry.pyis one complete Coze remote call and the main time cost of a single Q&A. The default is one call per question: capture the entire stdout into memory/a variable and run the sha256 check against that SAME captured result — re-callingentry.pypurely to perform the check is strictly forbidden (it sends an identical request to the Coze backend again, doubling the latency; in practice this once caused "the same question sent 2–3 times, a single Q&A dragging to 4 minutes"). The single exception is decision-table steps 2–3: at most one re-send to repair a broken CAPTURE (mojibake/truncation/destroyed structure), with the reason stated to the user — never for a clean-text mismatch (step 4 → stop) and never without capture evidence. If you need to persist output, write the stdout already captured from the first run rather than re-running.
Verification discipline (L3, 2026-09-25): The sha256 integrity check MUST be performed inline against the stdout captured in memory from the SAME entry.py call — i.e. immediately after capture, do re.search + hashlib.sha256 comparison in memory. Forbidden: writing stdout to a temp file just for the check, extra tool round-trips, or post-hoc temp-file cleanup. One extra file write/read/delete round-trip only adds agent-side latency and contributes nothing to the answer itself.
Performance Optimization & Deployment Notes (L1/L2/L3, 2026-09-26)
This skill has implemented three performance optimizations; see PERF_SOP.md for details:
- L1 Resident orchestration service (
scripts/serve.py+entry.pyclient): heavy modules are warmed up once, so local overhead → 0. - L2 Parallel warm-up (
scripts/orchestrate.py): eliminates stacked serial imports. - L3 Inline verification discipline (see "Verification discipline" above): sha256 must be done inline in memory; writing temp files is forbidden.
⚠️ Deployment restriction (important): the skill directory lives on a network share whose ACL is creator-owner — you can create new files, but overwriting pre-installed existing files raises PermissionError 13, and deletion is also blocked by a trash hook. Therefore, any change to scripts/*.py / SKILL.md should be written as a same-directory .new copy, then overwritten on the user's local machine with move /Y (command in PERF_SOP.md §4, or run scripts/_perf_deploy.py).
Version drift: the resident service caches code; after updating skill files, the old service still runs old code until its 600s idle timeout. entry.py has a built-in /version probe that, before reusing an old service, compares the local orchestrate.py mtime — if the local copy is newer it auto-shuts-down the old service and launches the new version, with no manual restart needed.
Requirements
| Item | Requirement |
|---|---|
| Runtime | python3 stdlib only |
| Sibling skills | Tier A · listed on SkillHub (non-confidential input, publicly released): ct-registry / ct-safety / ct-literature / ct-samplesize — auto-routed via Coze need_tool (entry.py handles card-inline execution). Tier A · NOT yet listed: ct-pipeline / ct-synthdata → unpublished_a, answer from Coze draft. Tier B (confidential input, not publicly released): ct-protocol / ct-csr / ct-analysis / ct-sdtm / ct-eligibility etc. — never installable. |
| Refiner (Coze) | scripts/entry.py (internal orchestrator via scripts/orchestrate.py) POSTs payload to ct-advisor.coze.site/run. Credential embedded in adapters/coze_token_embedded.py (obfuscated public token — keep as-is). 90s timeout / 300s long timeout. |
| External Refs | teal (insightsengineering) — clinical-data interactive exploration framework; Docling — structured PDF parsing. See "External Tool References". |
Attachment handling (docx / xlsx / pptx · doc / xls / ppt · pdf)
Single gate = 5 MB, decided FIRST inside entry.py (v1.1.0 workflow correction, 2026-09-25). BOTH paths deliver a FILE to Coze through the same doc_context channel.
| Size | Behavior | Channel |
|---|---|---|
< 5 MB |
Upload the original file directly — never converted locally | doc_memory.build_file_payload(allow_upload=True) → Coze /upload_file → doc_context (mode=file_id); on upload failure auto-degrades to the base64 forward channel (mode=file) |
> 5 MB |
Convert to Markdown locally, then upload the resulting .md as the attachment — same doc_context channel — plus an ℹ️ notice that the cloud answers from the md version |
scripts/office_to_md.py (stdlib-only; OOXML) or direct read (.txt/.md/.csv/.tsv/.json); unsupported formats get an explicit user prompt |
Coze decodes the original file natively for any Office format (OLE2 .doc/.xls/.ppt included), so fidelity is higher than any local conversion — this is why the <5 MB path uploads instead of converting.
Governance pointer: layered conversion strategy, user prompts and confidentiality boundary are consolidated in ct-base §6.7; the shared converter lives in ct-base/scripts/office_to_md.py (vendored copy).
Deliverable boundary — the revised document is never returned (v1.24). This skill's deliverable is text only: it does not generate, export, or hand back a modified document file. When the user asks for the revised file back, state plainly on the first line that this feature is not available — only written suggestions are provided, then give the suggestions as text the user can copy.
Quality Gate & Stop Rules
Presentation rules (user-mandated, hard) — deliver only the answer text obtained by base64-decoding the payload between <<<CT_ANSWER_B64_START>>> and <<<CT_ANSWER_B64_END>>>. Never emit any workflow / process narration to the user.
🔔 Forward-mode user notice (the ONLY allowed process message) — entry.py emits a brief notice to stderr before firing the Coze call. The local LLM should not add any additional process chatter.
Outbound Authorization Gate
Runs automatically inside entry.py → scripts/orchestrate.py before each outbound HTTP call (agent never triggers it manually). Endpoint https://ct-advisor.coze.site/run is in auto_approve_endpoints by default, so it never prompts. If the user wants another endpoint whitelisted across sessions, the USER adds it explicitly to config.json — the agent MUST NOT edit config.json.
External Tool References (v1.3.4 · external authoritative assets)
- teal (insightsengineering,
install.packages('teal')) — clinical-data interactive exploration framework for building Shiny review apps; reference model for the data-review integration mode. - Docling (
pip install docling) — structured parsing of regulatory documents (protocol/IB/CSR PDFs → hierarchical Markdown/JSON); attachment-parsing enhancement layer. - Pinnacle 21 Validation Rules (Certara, public API
standards.pinnacle21.certara.net/validation-rules/<STD>, headerX-Requested-With: XMLHttpRequest) — industry-standard CDISC validation rule set; snapshots archived inct-base/references/pinnacle21_validation_rules/(SDTM 566 / SEND 581 / ADaM 456 / Define-XML 167). Authoritative benchmark for data-standard compliance Q&A; mirrored to Coze KB topicref-topic-p21-data-rules. - ICH M12 DDI Thresholds (Step 4/5 official PDFs archived alongside
ct-base/references/m12_thresholds.md) — CYP 50× / intestinal 10×, TDI 1.25, induction R-ratio, transporter Table 1, AUCR 0.80–1.25, GMR 90% CI 80–125%. Authoritative benchmark for DDI interaction Q&A; mirrored to Coze KB topicref-topic-m12-ddi.
Bug Report (§20.3 · ct-base)
On defect detection or explicit user request, adapters/bug_report.py offers a sanitized 11-key report to https://ct-bugreport.coze.site/run. Two-stage confirmation mandatory.
Error Self-Heal Protocol (stderr machine signals · 2026-10-15)
Scope — failure paths ONLY. On a successful run (well-formed <<<CT_ANSWER_B64_START>>>…<<<CT_ANSWER_B64_END>>> + checksum: on stdout, and no [ct-advisor][ERR] line on stderr), the agent delivers the answer and stops. The agent MUST NOT enter self-healing when no error was reported — no proactive health checks, no proactive dependency installs, no proactive config/env edits, no speculative retries. The [ct-advisor][ERR] signal is the only trigger.
Signal format (single line, stderr):
[ct-advisor][ERR] code=<CODE> auto_fixable=<true|false> retry_safe=<true|false> requires_network=<true|false> [detail="…"] [fix_cmd="…"]
Code → agent action:
| code | auto_fixable | retry_safe | requires_network | Agent action |
|---|---|---|---|---|
CONN_FAIL_PROXY |
false | true | true | Deliver the answer-block text verbatim (it carries numbered recovery steps). Offer one retry only after the user confirms the proxy/network is fixed. Never edit env or config yourself. |
CONN_FAIL_DIRECT |
false | true | true | Same as above. |
TIMEOUT |
false | false | true | Deliver verbatim. Never auto-retry — Coze likely already received the request; a blind retry duplicates it. |
AUTH_401 |
false | false | false | Deliver verbatim; explain the token must be renewed by the skill maintainer. Do not edit any skill file. |
COZE_HTTP_ERROR |
false | false | true | Deliver verbatim; a user-initiated retry after 1–2 minutes is acceptable. |
ORCH_FAIL |
false | false | false | Deliver verbatim; ask the user to report the detail field to the skill maintainer. |
Hard rules (violating any of these is a defect):
- No signal → no action. Absence of
[ct-advisor][ERR]means success; the agent must not probe, install, edit, or retry anything. fix_cmdmay be executed only whenauto_fixable=true(after the 2026-10-15 stdlib-urllib transport change, no code currently qualifies — the dependency-missing class was eliminated) and the user has explicitly confirmed. Execute it verbatim; never improvise commands; never edit skill files orconfig.json(same red line as the Outbound Authorization Gate).- At most one retry per user turn, only after a successful fix, and only when
retry_safe=true. requires_network=truemeans the fix itself needs working outbound access — resolve network-class errors first.- Self-healing covers transport repair only. It never licenses local answer generation — the coze-only red line stands.
- If stderr carries an
[ERR]line but stdout has no answer block, treat the turn as failed: relay the signal's meaning plus any human-readable guidance. Never fabricate an answer. - Signals are emitted from failure paths only (
adapters/errsignal.py::emit_err_signal). If an[ERR]line appears on a run that returned a valid answer, treat it as a skill defect and report it.
Note on [DEP-MISS]: before 2026-10-15 a missing requests library surfaced as [DEP-MISS] + an install prompt. The transport now uses stdlib urllib, so this failure class no longer exists; the handler remains as dormant defensive code.
