dsh-aris-panel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/README_CN.md +87 -0
- package/dsh/checkout.patch.yml +38 -0
- package/dsh/client.js +634 -0
- package/dsh/cordis.patch.yml +44 -0
- package/dsh/index.mjs +76 -0
- package/dsh/run-status.mjs +182 -0
- package/dsh/scope-limits.mjs +50 -0
- package/dsh/workbench.mjs +291 -0
- package/mcp-servers/claude-review/README.md +93 -0
- package/mcp-servers/claude-review/run_with_claude_aws.sh +49 -0
- package/mcp-servers/claude-review/server.py +718 -0
- package/mcp-servers/codex-image2/README.md +65 -0
- package/mcp-servers/codex-image2/server.py +893 -0
- package/mcp-servers/feishu-bridge/requirements.txt +1 -0
- package/mcp-servers/feishu-bridge/server.py +240 -0
- package/mcp-servers/gemini-review/README.md +171 -0
- package/mcp-servers/gemini-review/server.py +1856 -0
- package/mcp-servers/llm-chat/requirements.txt +1 -0
- package/mcp-servers/llm-chat/server.py +664 -0
- package/mcp-servers/manual-review/README.md +133 -0
- package/mcp-servers/manual-review/server.py +910 -0
- package/mcp-servers/manual-review/ui.html +279 -0
- package/mcp-servers/minimax-chat/requirements.txt +1 -0
- package/mcp-servers/minimax-chat/server.py +381 -0
- package/package.json +51 -0
- package/skills/ablation-planner/SKILL.md +123 -0
- package/skills/alphaxiv/SKILL.md +196 -0
- package/skills/analyze-results/SKILL.md +46 -0
- package/skills/arxiv/SKILL.md +248 -0
- package/skills/auto-paper-improvement-loop/SKILL.md +651 -0
- package/skills/auto-review-loop/SKILL.md +1137 -0
- package/skills/auto-review-loop-llm/SKILL.md +259 -0
- package/skills/auto-review-loop-minimax/SKILL.md +302 -0
- package/skills/citation-audit/SKILL.md +502 -0
- package/skills/claims-drafting/SKILL.md +227 -0
- package/skills/comm-lit-review/SKILL.md +297 -0
- package/skills/deepxiv/SKILL.md +263 -0
- package/skills/dse-loop/SKILL.md +296 -0
- package/skills/embodiment-description/SKILL.md +129 -0
- package/skills/exa-search/SKILL.md +205 -0
- package/skills/experiment-audit/SKILL.md +311 -0
- package/skills/experiment-bridge/SKILL.md +376 -0
- package/skills/experiment-plan/SKILL.md +249 -0
- package/skills/experiment-queue/SKILL.md +431 -0
- package/skills/experiment-queue/scripts/build_manifest.py +142 -0
- package/skills/experiment-queue/scripts/queue_manager.py +433 -0
- package/skills/feishu-notify/SKILL.md +156 -0
- package/skills/figure-description/SKILL.md +138 -0
- package/skills/figure-spec/SKILL.md +262 -0
- package/skills/figure-spec/scripts/figure_renderer.py +799 -0
- package/skills/formula-derivation/SKILL.md +280 -0
- package/skills/gemini-search/SKILL.md +231 -0
- package/skills/grant-proposal/SKILL.md +698 -0
- package/skills/idea-creator/SKILL.md +542 -0
- package/skills/idea-discovery/SKILL.md +521 -0
- package/skills/idea-discovery-robot/SKILL.md +363 -0
- package/skills/integrity-forensics/SKILL.md +284 -0
- package/skills/interview-cheatsheet/SKILL.md +245 -0
- package/skills/invention-structuring/SKILL.md +188 -0
- package/skills/jurisdiction-format/SKILL.md +192 -0
- package/skills/kill-argument/SKILL.md +437 -0
- package/skills/mermaid-diagram/SKILL.md +419 -0
- package/skills/meta-apply/SKILL.md +141 -0
- package/skills/meta-optimize/SKILL.md +437 -0
- package/skills/monitor-experiment/SKILL.md +140 -0
- package/skills/novelty-check/SKILL.md +101 -0
- package/skills/openalex/SKILL.md +237 -0
- package/skills/overleaf-sync/SKILL.md +220 -0
- package/skills/paper-claim-audit/SKILL.md +348 -0
- package/skills/paper-compile/SKILL.md +266 -0
- package/skills/paper-figure/SKILL.md +312 -0
- package/skills/paper-illustration/SKILL.md +736 -0
- package/skills/paper-illustration-image2/SKILL.md +391 -0
- package/skills/paper-illustration-image2/scripts/paper_illustration_image2.py +255 -0
- package/skills/paper-plan/SKILL.md +386 -0
- package/skills/paper-poster/SKILL.md +19 -0
- package/skills/paper-poster-html/DESIGN_FINAL.md +176 -0
- package/skills/paper-poster-html/IMPLEMENTATION_CONVENTIONS.md +161 -0
- package/skills/paper-poster-html/LICENSES/posterly-MIT.txt +21 -0
- package/skills/paper-poster-html/NOTICE.md +57 -0
- package/skills/paper-poster-html/SKILL.md +323 -0
- package/skills/paper-poster-html/scripts/_posterly/__init__.py +0 -0
- package/skills/paper-poster-html/scripts/_posterly/canvas.py +200 -0
- package/skills/paper-poster-html/scripts/_posterly/measure.py +588 -0
- package/skills/paper-poster-html/scripts/_posterly/polish.py +498 -0
- package/skills/paper-poster-html/scripts/_posterly/preflight.py +489 -0
- package/skills/paper-poster-html/scripts/_posterly/render.py +215 -0
- package/skills/paper-poster-html/scripts/_posterly/textutil.py +16 -0
- package/skills/paper-poster-html/scripts/_posterly/verify_final.py +171 -0
- package/skills/paper-poster-html/scripts/asset_check.py +897 -0
- package/skills/paper-poster-html/scripts/extract_pdf_figures.py +666 -0
- package/skills/paper-poster-html/scripts/poster_check.py +251 -0
- package/skills/paper-poster-html/scripts/preprocess_figures.py +238 -0
- package/skills/paper-poster-html/scripts/render_preview.py +217 -0
- package/skills/paper-poster-html/scripts/run_gates.py +556 -0
- package/skills/paper-poster-html/scripts/style_check.py +1324 -0
- package/skills/paper-poster-html/templates/COMPONENTS.md +462 -0
- package/skills/paper-poster-html/templates/README.md +170 -0
- package/skills/paper-poster-html/templates/landscape_4col.html +1032 -0
- package/skills/paper-poster-html/templates/landscape_hero.html +1046 -0
- package/skills/paper-poster-html/templates/portrait_2col.html +947 -0
- package/skills/paper-poster-html/templates/tokens/acl.json +9 -0
- package/skills/paper-poster-html/templates/tokens/cvpr.json +9 -0
- package/skills/paper-poster-html/templates/tokens/generic.json +9 -0
- package/skills/paper-poster-html/templates/tokens/iclr.json +9 -0
- package/skills/paper-poster-html/templates/tokens/icml.json +9 -0
- package/skills/paper-poster-html/templates/tokens/neurips.json +9 -0
- package/skills/paper-slides/SKILL.md +635 -0
- package/skills/paper-talk/SKILL.md +381 -0
- package/skills/paper-write/SKILL.md +604 -0
- package/skills/paper-write/templates/IEEEtran.bst +2409 -0
- package/skills/paper-write/templates/IEEEtran.cls +6347 -0
- package/skills/paper-write/templates/iclr2026.tex +84 -0
- package/skills/paper-write/templates/icml2025.tex +87 -0
- package/skills/paper-write/templates/ieee_conference.tex +89 -0
- package/skills/paper-write/templates/ieee_journal.tex +93 -0
- package/skills/paper-write/templates/math_commands.tex +48 -0
- package/skills/paper-write/templates/neurips2025.tex +80 -0
- package/skills/paper-writing/SKILL.md +916 -0
- package/skills/patent-novelty-check/SKILL.md +153 -0
- package/skills/patent-pipeline/SKILL.md +344 -0
- package/skills/patent-review/SKILL.md +203 -0
- package/skills/pixel-art/SKILL.md +137 -0
- package/skills/prior-art-search/SKILL.md +146 -0
- package/skills/proof-checker/SKILL.md +866 -0
- package/skills/proof-orchestrator/NOTICE.md +24 -0
- package/skills/proof-orchestrator/SKILL.md +254 -0
- package/skills/proof-orchestrator/references/audit-output-contract.md +126 -0
- package/skills/proof-orchestrator/references/deepseek-routing.md +74 -0
- package/skills/proof-orchestrator/references/dispatch-prompts.md +227 -0
- package/skills/proof-orchestrator/references/notation-audit.md +135 -0
- package/skills/proof-orchestrator/references/proof-audit-rubric.md +70 -0
- package/skills/proof-orchestrator/references/stress-tests.md +38 -0
- package/skills/proof-writer/SKILL.md +223 -0
- package/skills/qzcli/SKILL.md +324 -0
- package/skills/rebuttal/SKILL.md +376 -0
- package/skills/render-html/SKILL.md +316 -0
- package/skills/render-html/scripts/render_html.py +1006 -0
- package/skills/render-html/scripts/templates/academic.html +703 -0
- package/skills/render-html/scripts/templates/dashboard.html +333 -0
- package/skills/research-lit/SKILL.md +756 -0
- package/skills/research-pipeline/SKILL.md +384 -0
- package/skills/research-refine/SKILL.md +770 -0
- package/skills/research-refine-pipeline/SKILL.md +186 -0
- package/skills/research-review/SKILL.md +198 -0
- package/skills/research-wiki/SKILL.md +461 -0
- package/skills/resubmit-pipeline/SKILL.md +447 -0
- package/skills/result-to-claim/SKILL.md +311 -0
- package/skills/run-experiment/SKILL.md +313 -0
- package/skills/semantic-scholar/SKILL.md +236 -0
- package/skills/serverless-modal/SKILL.md +335 -0
- package/skills/shared-references/acceptance-gate.md +324 -0
- package/skills/shared-references/assurance-contract.md +248 -0
- package/skills/shared-references/capture-antipatterns.md +78 -0
- package/skills/shared-references/citation-discipline.md +583 -0
- package/skills/shared-references/compute-env-contract.md +163 -0
- package/skills/shared-references/effort-contract.md +183 -0
- package/skills/shared-references/evidence-precheck.md +65 -0
- package/skills/shared-references/experiment-integrity.md +49 -0
- package/skills/shared-references/external-cadence.md +326 -0
- package/skills/shared-references/fan-out-pattern.md +366 -0
- package/skills/shared-references/injection-hygiene.md +127 -0
- package/skills/shared-references/integration-contract.md +461 -0
- package/skills/shared-references/output-composition.md +93 -0
- package/skills/shared-references/output-language.md +45 -0
- package/skills/shared-references/output-manifest.md +49 -0
- package/skills/shared-references/output-versioning.md +111 -0
- package/skills/shared-references/patent-format-cn.md +199 -0
- package/skills/shared-references/patent-format-ep.md +173 -0
- package/skills/shared-references/patent-format-us.md +161 -0
- package/skills/shared-references/patent-writing-principles.md +197 -0
- package/skills/shared-references/prior-art-databases.md +141 -0
- package/skills/shared-references/resumable-runs.md +109 -0
- package/skills/shared-references/review-scope-limits.md +81 -0
- package/skills/shared-references/review-tracing.md +391 -0
- package/skills/shared-references/reviewer-independence.md +79 -0
- package/skills/shared-references/reviewer-routing.md +852 -0
- package/skills/shared-references/skill-governance.md +104 -0
- package/skills/shared-references/taste-calibration.md +85 -0
- package/skills/shared-references/venue-checklists.md +114 -0
- package/skills/shared-references/wiki-helper-resolution.md +134 -0
- package/skills/shared-references/writing-principles.md +525 -0
- package/skills/skills-codex/README.md +102 -0
- package/skills/skills-codex/README_CN.md +100 -0
- package/skills/skills-codex/ablation-planner/SKILL.md +126 -0
- package/skills/skills-codex/alphaxiv/SKILL.md +186 -0
- package/skills/skills-codex/analyze-results/SKILL.md +45 -0
- package/skills/skills-codex/arxiv/SKILL.md +210 -0
- package/skills/skills-codex/auto-paper-improvement-loop/SKILL.md +574 -0
- package/skills/skills-codex/auto-review-loop/SKILL.md +500 -0
- package/skills/skills-codex/auto-review-loop-llm/SKILL.md +247 -0
- package/skills/skills-codex/auto-review-loop-minimax/SKILL.md +290 -0
- package/skills/skills-codex/citation-audit/SKILL.md +504 -0
- package/skills/skills-codex/claims-drafting/SKILL.md +239 -0
- package/skills/skills-codex/comm-lit-review/SKILL.md +299 -0
- package/skills/skills-codex/comm-lit-review/references/domain-taxonomy.md +57 -0
- package/skills/skills-codex/comm-lit-review/references/output-template.md +37 -0
- package/skills/skills-codex/comm-lit-review/references/source-policy.md +99 -0
- package/skills/skills-codex/comm-lit-review/references/venue-tiering.md +112 -0
- package/skills/skills-codex/deepxiv/SKILL.md +142 -0
- package/skills/skills-codex/dse-loop/SKILL.md +285 -0
- package/skills/skills-codex/embodiment-description/SKILL.md +129 -0
- package/skills/skills-codex/exa-search/SKILL.md +192 -0
- package/skills/skills-codex/experiment-audit/SKILL.md +286 -0
- package/skills/skills-codex/experiment-bridge/SKILL.md +356 -0
- package/skills/skills-codex/experiment-plan/SKILL.md +249 -0
- package/skills/skills-codex/experiment-queue/SKILL.md +401 -0
- package/skills/skills-codex/feishu-notify/SKILL.md +155 -0
- package/skills/skills-codex/figure-description/SKILL.md +138 -0
- package/skills/skills-codex/figure-spec/SKILL.md +252 -0
- package/skills/skills-codex/formula-derivation/SKILL.md +280 -0
- package/skills/skills-codex/gemini-search/SKILL.md +205 -0
- package/skills/skills-codex/grant-proposal/SKILL.md +626 -0
- package/skills/skills-codex/idea-creator/SKILL.md +405 -0
- package/skills/skills-codex/idea-discovery/SKILL.md +475 -0
- package/skills/skills-codex/idea-discovery-robot/SKILL.md +362 -0
- package/skills/skills-codex/integrity-forensics/SKILL.md +106 -0
- package/skills/skills-codex/interview-cheatsheet/SKILL.md +245 -0
- package/skills/skills-codex/invention-structuring/SKILL.md +188 -0
- package/skills/skills-codex/jurisdiction-format/SKILL.md +192 -0
- package/skills/skills-codex/kill-argument/SKILL.md +403 -0
- package/skills/skills-codex/mermaid-diagram/SKILL.md +379 -0
- package/skills/skills-codex/meta-apply/SKILL.md +154 -0
- package/skills/skills-codex/meta-optimize/SKILL.md +348 -0
- package/skills/skills-codex/monitor-experiment/SKILL.md +98 -0
- package/skills/skills-codex/novelty-check/SKILL.md +89 -0
- package/skills/skills-codex/openalex/SKILL.md +228 -0
- package/skills/skills-codex/overleaf-sync/SKILL.md +220 -0
- package/skills/skills-codex/paper-claim-audit/SKILL.md +350 -0
- package/skills/skills-codex/paper-compile/SKILL.md +253 -0
- package/skills/skills-codex/paper-figure/SKILL.md +311 -0
- package/skills/skills-codex/paper-illustration/SKILL.md +690 -0
- package/skills/skills-codex/paper-illustration-image2/SKILL.md +383 -0
- package/skills/skills-codex/paper-illustration-image2/scripts/paper_illustration_image2.py +255 -0
- package/skills/skills-codex/paper-plan/SKILL.md +278 -0
- package/skills/skills-codex/paper-poster/SKILL.md +19 -0
- package/skills/skills-codex/paper-poster-html/SKILL.md +377 -0
- package/skills/skills-codex/paper-slides/SKILL.md +571 -0
- package/skills/skills-codex/paper-talk/SKILL.md +381 -0
- package/skills/skills-codex/paper-write/SKILL.md +411 -0
- package/skills/skills-codex/paper-write/templates/IEEEtran.bst +2409 -0
- package/skills/skills-codex/paper-write/templates/IEEEtran.cls +6347 -0
- package/skills/skills-codex/paper-write/templates/aaai2026.bst +1493 -0
- package/skills/skills-codex/paper-write/templates/aaai2026.sty +315 -0
- package/skills/skills-codex/paper-write/templates/aaai2026.tex +952 -0
- package/skills/skills-codex/paper-write/templates/acl.sty +312 -0
- package/skills/skills-codex/paper-write/templates/acl2026.tex +377 -0
- package/skills/skills-codex/paper-write/templates/acl_natbib.bst +1940 -0
- package/skills/skills-codex/paper-write/templates/acm.bst +3081 -0
- package/skills/skills-codex/paper-write/templates/acm_mm2026.tex +204 -0
- package/skills/skills-codex/paper-write/templates/acmart.cls +3520 -0
- package/skills/skills-codex/paper-write/templates/cvpr.bst +1448 -0
- package/skills/skills-codex/paper-write/templates/cvpr.sty +508 -0
- package/skills/skills-codex/paper-write/templates/cvpr2026.tex +63 -0
- package/skills/skills-codex/paper-write/templates/iclr2026.tex +84 -0
- package/skills/skills-codex/paper-write/templates/iclr2026_conference.bst +1440 -0
- package/skills/skills-codex/paper-write/templates/iclr2026_conference.sty +246 -0
- package/skills/skills-codex/paper-write/templates/icml2026.sty +767 -0
- package/skills/skills-codex/paper-write/templates/icml2026.tex +662 -0
- package/skills/skills-codex/paper-write/templates/ieee_conference.tex +89 -0
- package/skills/skills-codex/paper-write/templates/ieee_journal.tex +93 -0
- package/skills/skills-codex/paper-write/templates/math_commands.tex +48 -0
- package/skills/skills-codex/paper-write/templates/neurips2026.tex +493 -0
- package/skills/skills-codex/paper-write/templates/neurips_2026.sty +437 -0
- package/skills/skills-codex/paper-writing/SKILL.md +731 -0
- package/skills/skills-codex/patent-novelty-check/SKILL.md +153 -0
- package/skills/skills-codex/patent-pipeline/SKILL.md +344 -0
- package/skills/skills-codex/patent-review/SKILL.md +202 -0
- package/skills/skills-codex/pixel-art/SKILL.md +139 -0
- package/skills/skills-codex/prior-art-search/SKILL.md +146 -0
- package/skills/skills-codex/proof-checker/SKILL.md +554 -0
- package/skills/skills-codex/proof-orchestrator/SKILL.md +260 -0
- package/skills/skills-codex/proof-orchestrator/references/audit-output-contract.md +126 -0
- package/skills/skills-codex/proof-orchestrator/references/deepseek-routing.md +76 -0
- package/skills/skills-codex/proof-orchestrator/references/dispatch-prompts.md +227 -0
- package/skills/skills-codex/proof-orchestrator/references/notation-audit.md +135 -0
- package/skills/skills-codex/proof-orchestrator/references/proof-audit-rubric.md +70 -0
- package/skills/skills-codex/proof-orchestrator/references/stress-tests.md +38 -0
- package/skills/skills-codex/proof-writer/SKILL.md +222 -0
- package/skills/skills-codex/qzcli/SKILL.md +324 -0
- package/skills/skills-codex/rebuttal/SKILL.md +305 -0
- package/skills/skills-codex/render-html/SKILL.md +305 -0
- package/skills/skills-codex/render-html/scripts/__pycache__/render_html.cpython-314.pyc +0 -0
- package/skills/skills-codex/render-html/scripts/render_html.py +909 -0
- package/skills/skills-codex/render-html/scripts/templates/academic.html +342 -0
- package/skills/skills-codex/render-html/scripts/templates/dashboard.html +333 -0
- package/skills/skills-codex/research-lit/SKILL.md +464 -0
- package/skills/skills-codex/research-pipeline/SKILL.md +340 -0
- package/skills/skills-codex/research-refine/SKILL.md +721 -0
- package/skills/skills-codex/research-refine-pipeline/SKILL.md +186 -0
- package/skills/skills-codex/research-review/SKILL.md +135 -0
- package/skills/skills-codex/research-wiki/SKILL.md +421 -0
- package/skills/skills-codex/resubmit-pipeline/SKILL.md +444 -0
- package/skills/skills-codex/result-to-claim/SKILL.md +246 -0
- package/skills/skills-codex/run-experiment/SKILL.md +236 -0
- package/skills/skills-codex/semantic-scholar/SKILL.md +219 -0
- package/skills/skills-codex/serverless-modal/SKILL.md +335 -0
- package/skills/skills-codex/shared-references/acceptance-gate.md +336 -0
- package/skills/skills-codex/shared-references/assurance-contract.md +139 -0
- package/skills/skills-codex/shared-references/capture-antipatterns.md +84 -0
- package/skills/skills-codex/shared-references/citation-discipline.md +452 -0
- package/skills/skills-codex/shared-references/compute-env-contract.md +163 -0
- package/skills/skills-codex/shared-references/effort-contract.md +143 -0
- package/skills/skills-codex/shared-references/evidence-precheck.md +73 -0
- package/skills/skills-codex/shared-references/experiment-integrity.md +49 -0
- package/skills/skills-codex/shared-references/external-cadence.md +334 -0
- package/skills/skills-codex/shared-references/fan-out-pattern.md +375 -0
- package/skills/skills-codex/shared-references/injection-hygiene.md +132 -0
- package/skills/skills-codex/shared-references/integration-contract.md +372 -0
- package/skills/skills-codex/shared-references/output-composition.md +98 -0
- package/skills/skills-codex/shared-references/output-language.md +45 -0
- package/skills/skills-codex/shared-references/output-manifest.md +40 -0
- package/skills/skills-codex/shared-references/output-versioning.md +111 -0
- package/skills/skills-codex/shared-references/patent-format-cn.md +199 -0
- package/skills/skills-codex/shared-references/patent-format-ep.md +173 -0
- package/skills/skills-codex/shared-references/patent-format-us.md +161 -0
- package/skills/skills-codex/shared-references/patent-writing-principles.md +197 -0
- package/skills/skills-codex/shared-references/prior-art-databases.md +141 -0
- package/skills/skills-codex/shared-references/resumable-runs.md +125 -0
- package/skills/skills-codex/shared-references/review-scope-limits.md +81 -0
- package/skills/skills-codex/shared-references/review-tracing.md +144 -0
- package/skills/skills-codex/shared-references/reviewer-independence.md +66 -0
- package/skills/skills-codex/shared-references/reviewer-routing.md +128 -0
- package/skills/skills-codex/shared-references/skill-governance.md +119 -0
- package/skills/skills-codex/shared-references/taste-calibration.md +90 -0
- package/skills/skills-codex/shared-references/venue-checklists.md +73 -0
- package/skills/skills-codex/shared-references/wiki-helper-resolution.md +69 -0
- package/skills/skills-codex/shared-references/writing-principles.md +525 -0
- package/skills/skills-codex/slides-polish/SKILL.md +563 -0
- package/skills/skills-codex/specification-writing/SKILL.md +211 -0
- package/skills/skills-codex/system-profile/SKILL.md +103 -0
- package/skills/skills-codex/training-check/SKILL.md +83 -0
- package/skills/skills-codex/vast-gpu/SKILL.md +394 -0
- package/skills/skills-codex/web-debug-search/SKILL.md +334 -0
- package/skills/skills-codex/wiki-enrich/SKILL.md +255 -0
- package/skills/skills-codex/writing-systems-papers/SKILL.md +184 -0
- package/skills/skills-codex-claude-review/README.md +79 -0
- package/skills/skills-codex-claude-review/README_CN.md +78 -0
- package/skills/skills-codex-claude-review/auto-paper-improvement-loop/SKILL.md +581 -0
- package/skills/skills-codex-claude-review/auto-review-loop/SKILL.md +510 -0
- package/skills/skills-codex-claude-review/novelty-check/SKILL.md +102 -0
- package/skills/skills-codex-claude-review/paper-figure/SKILL.md +319 -0
- package/skills/skills-codex-claude-review/paper-plan/SKILL.md +287 -0
- package/skills/skills-codex-claude-review/paper-write/SKILL.md +420 -0
- package/skills/skills-codex-claude-review/research-refine/SKILL.md +732 -0
- package/skills/skills-codex-claude-review/research-review/SKILL.md +149 -0
- package/skills/skills-codex-gemini-review/README.md +176 -0
- package/skills/skills-codex-gemini-review/README_CN.md +175 -0
- package/skills/skills-codex-gemini-review/auto-paper-improvement-loop/SKILL.md +331 -0
- package/skills/skills-codex-gemini-review/auto-review-loop/SKILL.md +304 -0
- package/skills/skills-codex-gemini-review/grant-proposal/SKILL.md +630 -0
- package/skills/skills-codex-gemini-review/idea-creator/SKILL.md +263 -0
- package/skills/skills-codex-gemini-review/idea-discovery/SKILL.md +275 -0
- package/skills/skills-codex-gemini-review/idea-discovery-robot/SKILL.md +365 -0
- package/skills/skills-codex-gemini-review/novelty-check/SKILL.md +92 -0
- package/skills/skills-codex-gemini-review/paper-figure/SKILL.md +289 -0
- package/skills/skills-codex-gemini-review/paper-plan/SKILL.md +265 -0
- package/skills/skills-codex-gemini-review/paper-poster-html/SKILL.md +102 -0
- package/skills/skills-codex-gemini-review/paper-slides/SKILL.md +582 -0
- package/skills/skills-codex-gemini-review/paper-write/SKILL.md +346 -0
- package/skills/skills-codex-gemini-review/paper-writing/SKILL.md +312 -0
- package/skills/skills-codex-gemini-review/research-refine/SKILL.md +674 -0
- package/skills/skills-codex-gemini-review/research-review/SKILL.md +112 -0
- package/skills/slides-polish/SKILL.md +565 -0
- package/skills/specification-writing/SKILL.md +211 -0
- package/skills/system-profile/SKILL.md +103 -0
- package/skills/training-check/SKILL.md +132 -0
- package/skills/vast-gpu/SKILL.md +394 -0
- package/skills/web-debug-search/SKILL.md +334 -0
- package/skills/wiki-enrich/SKILL.md +257 -0
- package/skills/writing-systems-papers/SKILL.md +184 -0
- package/templates/CLAUDE_MD_TEMPLATE.md +29 -0
- package/templates/EXPERIMENT_LOG_TEMPLATE.md +47 -0
- package/templates/EXPERIMENT_PLAN_TEMPLATE.md +51 -0
- package/templates/EXPERIMENT_PLAN_TEMPLATE_CN.md +53 -0
- package/templates/FINDINGS_TEMPLATE.md +52 -0
- package/templates/IDEA_CANDIDATES_TEMPLATE.md +47 -0
- package/templates/IDEA_CANDIDATES_TEMPLATE_CN.md +47 -0
- package/templates/INVENTION_BRIEF_TEMPLATE.md +87 -0
- package/templates/MANIFEST_TEMPLATE.md +7 -0
- package/templates/NARRATIVE_REPORT_TEMPLATE.md +49 -0
- package/templates/PAPER_PLAN_TEMPLATE.md +47 -0
- package/templates/PATENT_CLAIMS_TEMPLATE.md +78 -0
- package/templates/PATENT_SPECIFICATION_TEMPLATE.md +68 -0
- package/templates/README.md +57 -0
- package/templates/RESEARCH_BRIEF_TEMPLATE.md +35 -0
- package/templates/RESEARCH_BRIEF_TEMPLATE_CN.md +41 -0
- package/templates/RESEARCH_CONTRACT_TEMPLATE.md +60 -0
- package/templates/claude-hooks/corpus_write_guard.json +16 -0
- package/templates/claude-hooks/corpus_write_guard.py +85 -0
- package/templates/claude-hooks/meta_logging.json +74 -0
- package/templates/gitignore-trace.txt +3 -0
- package/tools/__pycache__/check_skills_inventory.cpython-314.pyc +0 -0
- package/tools/arxiv_fetch.py +311 -0
- package/tools/capture_filter.py +126 -0
- package/tools/check_skills_inventory.py +273 -0
- package/tools/convert_skills_to_llm_chat.py +282 -0
- package/tools/copilot_native_evidence.py +818 -0
- package/tools/deepxiv_fetch.py +213 -0
- package/tools/evidence_check.py +212 -0
- package/tools/exa_search.py +425 -0
- package/tools/experiment_queue/README.md +118 -0
- package/tools/experiment_queue/build_manifest.py +44 -0
- package/tools/experiment_queue/queue_manager.py +44 -0
- package/tools/extract_paper_style.py +560 -0
- package/tools/figure_renderer.py +69 -0
- package/tools/forensics_gate.py +669 -0
- package/tools/generate_codex_claude_review_overrides.py +299 -0
- package/tools/idea_discovery_gate.py +256 -0
- package/tools/install_aris.ps1 +1372 -0
- package/tools/install_aris.sh +1370 -0
- package/tools/install_aris_codex.sh +1023 -0
- package/tools/install_aris_copilot.sh +1052 -0
- package/tools/iteration_log.py +143 -0
- package/tools/lint_skills_helpers.sh +84 -0
- package/tools/meta_opt/check_ready.sh +80 -0
- package/tools/meta_opt/log_event.sh +91 -0
- package/tools/meta_opt/trigger_eval.py +280 -0
- package/tools/meta_opt/trigger_evals.sample.json +28 -0
- package/tools/openalex_fetch.py +326 -0
- package/tools/overleaf_audit.sh +104 -0
- package/tools/overleaf_setup.sh +150 -0
- package/tools/paper_illustration_image2.py +62 -0
- package/tools/provenance.py +294 -0
- package/tools/research_wiki.py +1720 -0
- package/tools/review_gate.py +502 -0
- package/tools/run_state.py +399 -0
- package/tools/save_trace.sh +477 -0
- package/tools/semantic_scholar_fetch.py +438 -0
- package/tools/skill-groups.tsv +116 -0
- package/tools/skill_picker.py +238 -0
- package/tools/smart_update.ps1 +521 -0
- package/tools/smart_update.sh +591 -0
- package/tools/smart_update_codex.sh +419 -0
- package/tools/smart_update_copilot.sh +605 -0
- package/tools/threat_scan.py +222 -0
- package/tools/verify_paper_audits.sh +487 -0
- package/tools/verify_papers.py +613 -0
- package/tools/verify_wiki_coverage.sh +176 -0
- package/tools/watchdog.py +485 -0
|
@@ -0,0 +1,910 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Manual Review MCP Server for ARIS.
|
|
3
|
+
|
|
4
|
+
A human-in-the-loop reviewer bridge: when the pipeline needs cross-model
|
|
5
|
+
review, this server opens a browser page (or writes a file on headless Linux)
|
|
6
|
+
where the user can copy the prompt to a different-family model and paste the
|
|
7
|
+
response back.
|
|
8
|
+
|
|
9
|
+
Zero API cost. The reviewer must be a model this server can classify by family
|
|
10
|
+
(OpenAI, Anthropic, Google, DeepSeek, Moonshot/Kimi, Qwen) — an unclassifiable
|
|
11
|
+
name cannot be shown to differ from the executor's, so it cannot acquit.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import http.server
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import socketserver
|
|
21
|
+
import sys
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
import uuid
|
|
25
|
+
import webbrowser
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
# --- stdio setup (deferred to main() to allow safe import for testing) ---
|
|
31
|
+
_stdio_initialized = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _init_stdio():
|
|
35
|
+
global _stdio_initialized
|
|
36
|
+
if _stdio_initialized:
|
|
37
|
+
return
|
|
38
|
+
sys.stdout = os.fdopen(sys.stdout.fileno(), "wb", buffering=0)
|
|
39
|
+
sys.stdin = os.fdopen(sys.stdin.fileno(), "rb", buffering=0)
|
|
40
|
+
_stdio_initialized = True
|
|
41
|
+
|
|
42
|
+
# --- Configuration ---
|
|
43
|
+
SERVER_NAME = os.environ.get("MANUAL_REVIEW_SERVER_NAME", "manual-review")
|
|
44
|
+
DEFAULT_TIMEOUT_SEC = int(os.environ.get("MANUAL_REVIEW_TIMEOUT_SEC", "86400"))
|
|
45
|
+
MODE = os.environ.get("MANUAL_REVIEW_MODE", "browser") # "browser" or "file"
|
|
46
|
+
AUTO_OPEN = os.environ.get("MANUAL_REVIEW_AUTO_OPEN", "true").lower() in {"1", "true", "yes"}
|
|
47
|
+
PENDING_DIR = Path(os.environ.get("MANUAL_REVIEW_PENDING_DIR", ".aris/pending_review"))
|
|
48
|
+
DEBUG_LOG_RAW = os.environ.get("MANUAL_REVIEW_DEBUG_LOG", "").strip()
|
|
49
|
+
DEBUG_LOG = Path(DEBUG_LOG_RAW).expanduser() if DEBUG_LOG_RAW else None
|
|
50
|
+
DEFAULT_PORT = int(os.environ.get("MANUAL_REVIEW_PORT", "17900"))
|
|
51
|
+
MAX_PORT_ATTEMPTS = 10
|
|
52
|
+
|
|
53
|
+
# File-mode stability: require content unchanged across two reads with this gap
|
|
54
|
+
FILE_STABLE_INTERVAL_SEC = 3
|
|
55
|
+
FILE_POLL_INTERVAL_SEC = 2
|
|
56
|
+
|
|
57
|
+
# --- MCP Protocol ---
|
|
58
|
+
_use_ndjson = False
|
|
59
|
+
|
|
60
|
+
# --- Thread storage (in-memory, lives as long as the MCP server process) ---
|
|
61
|
+
_threads: dict[str, list[dict[str, str]]] = {}
|
|
62
|
+
|
|
63
|
+
# --- UI HTML (loaded once) ---
|
|
64
|
+
_UI_HTML: str | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def debug_log(message: str) -> None:
|
|
68
|
+
if DEBUG_LOG is None:
|
|
69
|
+
return
|
|
70
|
+
try:
|
|
71
|
+
DEBUG_LOG.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
with DEBUG_LOG.open("a", encoding="utf-8") as fh:
|
|
73
|
+
fh.write(f"[{utc_now()}] {message}\n")
|
|
74
|
+
except OSError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def utc_now() -> str:
|
|
79
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def model_family(model: str) -> str:
|
|
83
|
+
"""Derive a known provider family from a model identity, failing closed."""
|
|
84
|
+
name = (model or "").strip().lower()
|
|
85
|
+
families: set[str] = set()
|
|
86
|
+
if re.search(r"(^|[^a-z0-9])(gpt|chatgpt|codex|oracle|o1|o3|o4)([^a-z0-9]|$)", name):
|
|
87
|
+
families.add("openai")
|
|
88
|
+
if re.search(r"(^|[^a-z0-9])(claude|sonnet|opus|haiku|anthropic)([^a-z0-9]|$)", name):
|
|
89
|
+
families.add("anthropic")
|
|
90
|
+
if re.search(r"(^|[^a-z0-9])(gemini|google)([^a-z0-9]|$)", name):
|
|
91
|
+
families.add("google")
|
|
92
|
+
# The models MANUAL_REVIEW_GUIDE.md actually recommends to non-GPT users.
|
|
93
|
+
# [0-9.]* so versioned names (qwen3-max, qwen2.5-72b) still match.
|
|
94
|
+
if re.search(r"(^|[^a-z0-9])(deepseek)[0-9.]*([^a-z0-9]|$)", name):
|
|
95
|
+
families.add("deepseek")
|
|
96
|
+
if re.search(r"(^|[^a-z0-9])(kimi|moonshot)[0-9.]*([^a-z0-9]|$)", name):
|
|
97
|
+
families.add("moonshot")
|
|
98
|
+
if re.search(r"(^|[^a-z0-9])(qwen|tongyi)[0-9.]*([^a-z0-9]|$)", name):
|
|
99
|
+
families.add("qwen")
|
|
100
|
+
return next(iter(families)) if len(families) == 1 else "unknown"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def reviewer_model_from_response(response: str) -> str | None:
|
|
104
|
+
"""Read the mandatory first-line identity without trusting prose later on."""
|
|
105
|
+
first_line = response.splitlines()[0].strip() if response.splitlines() else ""
|
|
106
|
+
match = re.fullmatch(r"Reviewer-Model:\s*(\S(?:.*\S)?)", first_line, re.IGNORECASE)
|
|
107
|
+
return match.group(1).strip() if match else None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def validate_reviewer_identity(response: str, config: dict) -> str | None:
|
|
111
|
+
"""Return an error when strict cross-family manual review cannot be proven."""
|
|
112
|
+
if not config.get("require_reviewer_model"):
|
|
113
|
+
return None
|
|
114
|
+
executor_model = str(config.get("executor_model") or "").strip()
|
|
115
|
+
executor_family = model_family(executor_model)
|
|
116
|
+
if executor_family == "unknown":
|
|
117
|
+
return "Cannot verify manual review: executor_model is missing or has an unknown family"
|
|
118
|
+
reviewer_model = reviewer_model_from_response(response)
|
|
119
|
+
if not reviewer_model:
|
|
120
|
+
return "Manual response must begin with: Reviewer-Model: <exact-model-id>"
|
|
121
|
+
reviewer_family = model_family(reviewer_model)
|
|
122
|
+
if reviewer_family == "unknown":
|
|
123
|
+
return f"Cannot verify manual reviewer model family: {reviewer_model}"
|
|
124
|
+
if reviewer_family == executor_family:
|
|
125
|
+
return (
|
|
126
|
+
"Manual reviewer must use a different model family: "
|
|
127
|
+
f"executor={executor_family}, reviewer={reviewer_family}"
|
|
128
|
+
)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def load_ui_html() -> str:
|
|
133
|
+
global _UI_HTML
|
|
134
|
+
if _UI_HTML is None:
|
|
135
|
+
ui_path = Path(__file__).parent / "ui.html"
|
|
136
|
+
_UI_HTML = ui_path.read_text(encoding="utf-8")
|
|
137
|
+
return _UI_HTML
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- MCP stdio transport ---
|
|
141
|
+
|
|
142
|
+
_send_lock = threading.Lock()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def send_response(response: dict[str, Any]) -> None:
|
|
146
|
+
global _use_ndjson
|
|
147
|
+
payload = json.dumps(response, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
148
|
+
debug_log(f"SEND {payload.decode('utf-8', errors='replace')[:200]}")
|
|
149
|
+
with _send_lock:
|
|
150
|
+
if _use_ndjson:
|
|
151
|
+
sys.stdout.write(payload + b"\n")
|
|
152
|
+
else:
|
|
153
|
+
header = f"Content-Length: {len(payload)}\r\n\r\n".encode("utf-8")
|
|
154
|
+
sys.stdout.write(header + payload)
|
|
155
|
+
sys.stdout.flush()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def read_message() -> dict[str, Any] | None:
|
|
159
|
+
global _use_ndjson
|
|
160
|
+
line = sys.stdin.readline()
|
|
161
|
+
if not line:
|
|
162
|
+
return None
|
|
163
|
+
line_text = line.decode("utf-8").rstrip("\r\n")
|
|
164
|
+
if line_text.lower().startswith("content-length:"):
|
|
165
|
+
try:
|
|
166
|
+
content_length = int(line_text.split(":", 1)[1].strip())
|
|
167
|
+
except ValueError:
|
|
168
|
+
return None
|
|
169
|
+
while True:
|
|
170
|
+
header_line = sys.stdin.readline()
|
|
171
|
+
if not header_line:
|
|
172
|
+
return None
|
|
173
|
+
if header_line in {b"\r\n", b"\n"}:
|
|
174
|
+
break
|
|
175
|
+
body = sys.stdin.read(content_length)
|
|
176
|
+
try:
|
|
177
|
+
return json.loads(body.decode("utf-8"))
|
|
178
|
+
except json.JSONDecodeError:
|
|
179
|
+
return None
|
|
180
|
+
if line_text.startswith("{") or line_text.startswith("["):
|
|
181
|
+
_use_ndjson = True
|
|
182
|
+
try:
|
|
183
|
+
return json.loads(line_text)
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
return None
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# --- Thread management ---
|
|
190
|
+
|
|
191
|
+
def create_thread() -> str:
|
|
192
|
+
thread_id = uuid.uuid4().hex[:12]
|
|
193
|
+
_threads[thread_id] = []
|
|
194
|
+
return thread_id
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def append_exchange(thread_id: str, role: str, content: str) -> None:
|
|
198
|
+
if thread_id not in _threads:
|
|
199
|
+
_threads[thread_id] = []
|
|
200
|
+
_threads[thread_id].append({"role": role, "content": content})
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_history(thread_id: str) -> list[dict[str, str]]:
|
|
204
|
+
return _threads.get(thread_id, [])
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# --- Pending state file ---
|
|
208
|
+
|
|
209
|
+
def _pending_dir_for(thread_id: str) -> Path:
|
|
210
|
+
"""Per-thread pending directory to avoid clobbering in concurrent calls."""
|
|
211
|
+
return PENDING_DIR / thread_id
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
_pending_state_lock = threading.Lock()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def write_pending_state(url: str | None, thread_id: str, prompt_file: str | None) -> None:
|
|
218
|
+
pdir = _pending_dir_for(thread_id)
|
|
219
|
+
state = {
|
|
220
|
+
"status": "waiting",
|
|
221
|
+
"url": url,
|
|
222
|
+
"prompt_file": prompt_file,
|
|
223
|
+
"response_file": str(pdir / "response.md") if prompt_file else None,
|
|
224
|
+
"thread_id": thread_id,
|
|
225
|
+
"created_at": utc_now(),
|
|
226
|
+
}
|
|
227
|
+
pdir.mkdir(parents=True, exist_ok=True)
|
|
228
|
+
state_path = pdir / "pending_review.json"
|
|
229
|
+
state_path.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
230
|
+
with _pending_state_lock:
|
|
231
|
+
PENDING_DIR.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
(PENDING_DIR / "pending_review.json").write_text(
|
|
233
|
+
json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def clear_pending_state(thread_id: str | None = None,
|
|
238
|
+
keep_thread_dir: bool = False) -> None:
|
|
239
|
+
"""Idempotent, thread-safe pending state cleanup.
|
|
240
|
+
|
|
241
|
+
keep_thread_dir preserves the per-thread directory when the round ended on a
|
|
242
|
+
fixable authoring error: in file mode that directory holds the prompt AND the
|
|
243
|
+
reviewer response the user just pasted, and deleting it would throw away
|
|
244
|
+
their work over a wrong first line. It does not make the call resumable — the
|
|
245
|
+
retry is a new call with a new directory.
|
|
246
|
+
"""
|
|
247
|
+
# Clear per-thread dir
|
|
248
|
+
if thread_id and not keep_thread_dir:
|
|
249
|
+
pdir = _pending_dir_for(thread_id)
|
|
250
|
+
if pdir.exists():
|
|
251
|
+
import shutil
|
|
252
|
+
shutil.rmtree(pdir, ignore_errors=True)
|
|
253
|
+
# Clear top-level pointer — tolerate already-deleted
|
|
254
|
+
with _pending_state_lock:
|
|
255
|
+
try:
|
|
256
|
+
(PENDING_DIR / "pending_review.json").unlink()
|
|
257
|
+
except FileNotFoundError:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# --- Browser mode: HTTP server ---
|
|
263
|
+
|
|
264
|
+
class _ReviewSession:
|
|
265
|
+
"""Holds state for one review interaction."""
|
|
266
|
+
def __init__(self, prompt: str, config: dict, thread_id: str, history: list):
|
|
267
|
+
self.prompt = prompt
|
|
268
|
+
self.config = config
|
|
269
|
+
self.thread_id = thread_id
|
|
270
|
+
self.history = history
|
|
271
|
+
self.response: str | None = None
|
|
272
|
+
self.done = threading.Event()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class _PendingCall:
|
|
276
|
+
"""Per-call state for the active blocking review call. Replaces global
|
|
277
|
+
_pending_call_cancelled with per-call cancellation that cannot be
|
|
278
|
+
cleared by a new call."""
|
|
279
|
+
def __init__(self, request_id: Any):
|
|
280
|
+
self.request_id = request_id
|
|
281
|
+
self.thread: threading.Thread | None = None
|
|
282
|
+
self.cancel_event = threading.Event()
|
|
283
|
+
self.suppress_response = False
|
|
284
|
+
self.cancel_reason = "Manual review request was cancelled"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
_current_session: _ReviewSession | None = None
|
|
288
|
+
_active_server: socketserver.TCPServer | None = None
|
|
289
|
+
_active_server_lock = threading.Lock()
|
|
290
|
+
_auth_token: str | None = None
|
|
291
|
+
_pending_call: _PendingCall | None = None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _generate_token() -> str:
|
|
295
|
+
"""Generate a one-shot auth token for this review session."""
|
|
296
|
+
return uuid.uuid4().hex
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _check_token(handler) -> bool:
|
|
300
|
+
"""Validate auth token from query string or header. Returns True if valid."""
|
|
301
|
+
from urllib.parse import urlparse, parse_qs
|
|
302
|
+
parsed = urlparse(handler.path)
|
|
303
|
+
params = parse_qs(parsed.query)
|
|
304
|
+
token_values = params.get("token", [])
|
|
305
|
+
if token_values and token_values[0] == _auth_token:
|
|
306
|
+
return True
|
|
307
|
+
header_token = handler.headers.get("X-Review-Token", "")
|
|
308
|
+
if header_token == _auth_token:
|
|
309
|
+
return True
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _check_origin(handler) -> bool:
|
|
314
|
+
"""Defense-in-depth: reject browser requests with suspicious cross-site headers.
|
|
315
|
+
Token auth is still required; this is an additional layer."""
|
|
316
|
+
origin = handler.headers.get("Origin", "")
|
|
317
|
+
if origin:
|
|
318
|
+
expected = f"http://127.0.0.1:{handler.server.server_address[1]}"
|
|
319
|
+
if origin != expected:
|
|
320
|
+
return False
|
|
321
|
+
sec_fetch_site = handler.headers.get("Sec-Fetch-Site", "")
|
|
322
|
+
if sec_fetch_site and sec_fetch_site not in {"same-origin", "none"}:
|
|
323
|
+
return False
|
|
324
|
+
return True
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class _ReviewHandler(http.server.BaseHTTPRequestHandler):
|
|
328
|
+
def log_message(self, format, *args):
|
|
329
|
+
debug_log(f"HTTP {format % args}")
|
|
330
|
+
|
|
331
|
+
def _get_clean_path(self) -> str:
|
|
332
|
+
from urllib.parse import urlparse
|
|
333
|
+
return urlparse(self.path).path
|
|
334
|
+
|
|
335
|
+
def do_GET(self):
|
|
336
|
+
path = self._get_clean_path()
|
|
337
|
+
if path == "/":
|
|
338
|
+
if not _check_token(self):
|
|
339
|
+
self.send_error(403, "Invalid or missing token")
|
|
340
|
+
return
|
|
341
|
+
if not _check_origin(self):
|
|
342
|
+
self.send_error(403, "Cross-origin request blocked")
|
|
343
|
+
return
|
|
344
|
+
html = load_ui_html()
|
|
345
|
+
self.send_response(200)
|
|
346
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
347
|
+
self.end_headers()
|
|
348
|
+
self.wfile.write(html.encode("utf-8"))
|
|
349
|
+
elif path == "/api/context":
|
|
350
|
+
if not _check_token(self):
|
|
351
|
+
self.send_error(403, "Invalid or missing token")
|
|
352
|
+
return
|
|
353
|
+
if not _check_origin(self):
|
|
354
|
+
self.send_error(403, "Cross-origin request blocked")
|
|
355
|
+
return
|
|
356
|
+
session = _current_session
|
|
357
|
+
ctx = {
|
|
358
|
+
"prompt": session.prompt if session else "",
|
|
359
|
+
"config": session.config if session else {},
|
|
360
|
+
"threadId": session.thread_id if session else "",
|
|
361
|
+
"history": session.history if session else [],
|
|
362
|
+
}
|
|
363
|
+
self.send_response(200)
|
|
364
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
365
|
+
self.end_headers()
|
|
366
|
+
self.wfile.write(json.dumps(ctx, ensure_ascii=False).encode("utf-8"))
|
|
367
|
+
else:
|
|
368
|
+
self.send_error(404)
|
|
369
|
+
|
|
370
|
+
def do_POST(self):
|
|
371
|
+
path = self._get_clean_path()
|
|
372
|
+
if path == "/api/submit":
|
|
373
|
+
if not _check_token(self):
|
|
374
|
+
self.send_error(403, "Invalid or missing token")
|
|
375
|
+
return
|
|
376
|
+
if not _check_origin(self):
|
|
377
|
+
self.send_error(403, "Cross-origin request blocked")
|
|
378
|
+
return
|
|
379
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
380
|
+
body = self.rfile.read(length).decode("utf-8")
|
|
381
|
+
try:
|
|
382
|
+
data = json.loads(body)
|
|
383
|
+
except json.JSONDecodeError:
|
|
384
|
+
self.send_error(400, "Invalid JSON")
|
|
385
|
+
return
|
|
386
|
+
response_text = data.get("response", "").strip()
|
|
387
|
+
if not response_text:
|
|
388
|
+
self.send_error(400, "Empty response")
|
|
389
|
+
return
|
|
390
|
+
session = _current_session
|
|
391
|
+
if session:
|
|
392
|
+
identity_error = validate_reviewer_identity(response_text, session.config)
|
|
393
|
+
if identity_error:
|
|
394
|
+
# JSON, not send_error's HTML page: the browser shows this text
|
|
395
|
+
# verbatim so a rejected paste is fixable in place.
|
|
396
|
+
payload = json.dumps({"error": identity_error}).encode("utf-8")
|
|
397
|
+
self.send_response(400)
|
|
398
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
399
|
+
self.send_header("Content-Length", str(len(payload)))
|
|
400
|
+
self.end_headers()
|
|
401
|
+
self.wfile.write(payload)
|
|
402
|
+
return
|
|
403
|
+
session.response = response_text
|
|
404
|
+
session.done.set()
|
|
405
|
+
self.send_response(200)
|
|
406
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
407
|
+
self.end_headers()
|
|
408
|
+
self.wfile.write(b'{"ok":true}')
|
|
409
|
+
else:
|
|
410
|
+
self.send_error(404)
|
|
411
|
+
|
|
412
|
+
def do_OPTIONS(self):
|
|
413
|
+
self.send_error(403, "CORS not allowed")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
FILE_MODE_WARNING = """# ARIS Manual Review - Cross-Model Warning
|
|
417
|
+
|
|
418
|
+
Use a reviewer from a DIFFERENT model family than the executor. A same-family response cannot satisfy an ARIS acceptance gate.
|
|
419
|
+
|
|
420
|
+
请使用与执行器不同模型家族的评审模型;同家族回复不能通过 ARIS 验收门。
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def file_mode_warning(config: dict) -> str:
|
|
428
|
+
header = FILE_MODE_WARNING
|
|
429
|
+
if config.get("require_reviewer_model"):
|
|
430
|
+
executor_model = str(config.get("executor_model") or "unknown")
|
|
431
|
+
header += (
|
|
432
|
+
f"Executor model: `{executor_model}` (derived family: `{model_family(executor_model)}`).\n\n"
|
|
433
|
+
"The response MUST begin with `Reviewer-Model: <exact-model-id>`.\n\n---\n\n"
|
|
434
|
+
)
|
|
435
|
+
return header
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def wait_for_browser_response(prompt: str, config: dict, thread_id: str,
|
|
439
|
+
history: list, cancel_event: threading.Event,
|
|
440
|
+
cancel_reason: str) -> tuple[str | None, str | None]:
|
|
441
|
+
global _current_session, _active_server, _auth_token
|
|
442
|
+
|
|
443
|
+
# Cleanup any leftover server from a previous interrupted call
|
|
444
|
+
with _active_server_lock:
|
|
445
|
+
if _active_server is not None:
|
|
446
|
+
try:
|
|
447
|
+
_active_server.shutdown()
|
|
448
|
+
_active_server.server_close()
|
|
449
|
+
except Exception:
|
|
450
|
+
pass
|
|
451
|
+
_active_server = None
|
|
452
|
+
session = _ReviewSession(prompt, config, thread_id, history)
|
|
453
|
+
_current_session = session
|
|
454
|
+
_auth_token = _generate_token()
|
|
455
|
+
|
|
456
|
+
# Try fixed port, increment on conflict
|
|
457
|
+
server = None
|
|
458
|
+
port = DEFAULT_PORT
|
|
459
|
+
for attempt in range(MAX_PORT_ATTEMPTS):
|
|
460
|
+
try:
|
|
461
|
+
socketserver.TCPServer.allow_reuse_address = True
|
|
462
|
+
srv = socketserver.TCPServer(("127.0.0.1", port), _ReviewHandler)
|
|
463
|
+
server = srv
|
|
464
|
+
break
|
|
465
|
+
except OSError:
|
|
466
|
+
port += 1
|
|
467
|
+
if server is None:
|
|
468
|
+
_current_session = None
|
|
469
|
+
return None, f"Could not bind to any port in range {DEFAULT_PORT}-{DEFAULT_PORT + MAX_PORT_ATTEMPTS - 1}"
|
|
470
|
+
|
|
471
|
+
with _active_server_lock:
|
|
472
|
+
_active_server = server
|
|
473
|
+
|
|
474
|
+
url = f"http://127.0.0.1:{port}?token={_auth_token}"
|
|
475
|
+
|
|
476
|
+
write_pending_state(url=url, thread_id=thread_id, prompt_file=None)
|
|
477
|
+
debug_log(f"HTTP server started on {url}")
|
|
478
|
+
|
|
479
|
+
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
480
|
+
server_thread.start()
|
|
481
|
+
|
|
482
|
+
if AUTO_OPEN:
|
|
483
|
+
try:
|
|
484
|
+
webbrowser.open(url)
|
|
485
|
+
except Exception:
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
response: str | None = None
|
|
489
|
+
error: str | None = None
|
|
490
|
+
|
|
491
|
+
try:
|
|
492
|
+
# Poll with short intervals; check cancel BEFORE success
|
|
493
|
+
deadline = time.monotonic() + DEFAULT_TIMEOUT_SEC
|
|
494
|
+
while time.monotonic() < deadline:
|
|
495
|
+
if cancel_event.is_set():
|
|
496
|
+
error = cancel_reason
|
|
497
|
+
break
|
|
498
|
+
if session.done.wait(timeout=1.0):
|
|
499
|
+
response = session.response
|
|
500
|
+
break
|
|
501
|
+
|
|
502
|
+
if error is None and response is None:
|
|
503
|
+
if cancel_event.is_set():
|
|
504
|
+
error = cancel_reason
|
|
505
|
+
elif not session.done.is_set():
|
|
506
|
+
error = f"Timed out after {DEFAULT_TIMEOUT_SEC}s waiting for manual review response"
|
|
507
|
+
finally:
|
|
508
|
+
# Worker thread owns cleanup: server, session, pending state
|
|
509
|
+
try:
|
|
510
|
+
server.shutdown()
|
|
511
|
+
except Exception:
|
|
512
|
+
pass
|
|
513
|
+
try:
|
|
514
|
+
server.server_close()
|
|
515
|
+
except Exception:
|
|
516
|
+
pass
|
|
517
|
+
with _active_server_lock:
|
|
518
|
+
if _active_server is server:
|
|
519
|
+
_active_server = None
|
|
520
|
+
if _current_session is session:
|
|
521
|
+
_current_session = None
|
|
522
|
+
clear_pending_state(thread_id)
|
|
523
|
+
|
|
524
|
+
return response, error
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
# --- File mode: prompt.md / response.md ---
|
|
528
|
+
|
|
529
|
+
def wait_for_file_response(prompt: str, config: dict, thread_id: str,
|
|
530
|
+
history: list, cancel_event: threading.Event,
|
|
531
|
+
cancel_reason: str) -> tuple[str | None, str | None]:
|
|
532
|
+
pdir = _pending_dir_for(thread_id)
|
|
533
|
+
pdir.mkdir(parents=True, exist_ok=True)
|
|
534
|
+
prompt_path = pdir / "prompt.md"
|
|
535
|
+
response_path = pdir / "response.md"
|
|
536
|
+
identity_error = None
|
|
537
|
+
|
|
538
|
+
# Clean up any stale response file
|
|
539
|
+
if response_path.exists():
|
|
540
|
+
response_path.unlink()
|
|
541
|
+
|
|
542
|
+
# Write prompt with cross-model warning
|
|
543
|
+
header = file_mode_warning(config)
|
|
544
|
+
header += f"<!-- thread: {thread_id} | config: {json.dumps(config)} -->\n\n"
|
|
545
|
+
if history:
|
|
546
|
+
header += "## Previous Exchanges\n\n"
|
|
547
|
+
for i, ex in enumerate(history):
|
|
548
|
+
header += f"### {'Prompt' if ex['role'] == 'user' else 'Response'} (Round {i // 2 + 1})\n\n"
|
|
549
|
+
header += ex["content"][:500] + ("..." if len(ex["content"]) > 500 else "") + "\n\n"
|
|
550
|
+
header += "---\n\n## Current Prompt\n\n"
|
|
551
|
+
# Atomic write (tmp + rename): watchers poll for prompt.md by NAME, and a
|
|
552
|
+
# bare write_text has an exists-but-empty window between create and flush —
|
|
553
|
+
# a reader (human tooling or the test suite) that wins that race sees "".
|
|
554
|
+
# os.replace is atomic on POSIX/Windows: the name appears only with full content.
|
|
555
|
+
prompt_tmp = pdir / ".prompt.md.tmp"
|
|
556
|
+
prompt_tmp.write_text(header + prompt, encoding="utf-8")
|
|
557
|
+
os.replace(prompt_tmp, prompt_path)
|
|
558
|
+
|
|
559
|
+
write_pending_state(url=None, thread_id=thread_id, prompt_file=str(prompt_path))
|
|
560
|
+
debug_log(f"File mode: prompt written to {prompt_path}, waiting for {response_path}")
|
|
561
|
+
|
|
562
|
+
# Poll for response file with stability check
|
|
563
|
+
deadline = time.monotonic() + DEFAULT_TIMEOUT_SEC
|
|
564
|
+
prev_content: str | None = None
|
|
565
|
+
response: str | None = None
|
|
566
|
+
error: str | None = None
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
while time.monotonic() < deadline:
|
|
570
|
+
if cancel_event.is_set():
|
|
571
|
+
error = cancel_reason
|
|
572
|
+
break
|
|
573
|
+
|
|
574
|
+
time.sleep(FILE_POLL_INTERVAL_SEC)
|
|
575
|
+
|
|
576
|
+
if cancel_event.is_set():
|
|
577
|
+
error = cancel_reason
|
|
578
|
+
break
|
|
579
|
+
|
|
580
|
+
if not response_path.exists():
|
|
581
|
+
prev_content = None
|
|
582
|
+
continue
|
|
583
|
+
try:
|
|
584
|
+
content = response_path.read_text(encoding="utf-8").strip()
|
|
585
|
+
except OSError:
|
|
586
|
+
prev_content = None
|
|
587
|
+
continue
|
|
588
|
+
if not content:
|
|
589
|
+
prev_content = None
|
|
590
|
+
continue
|
|
591
|
+
if content == prev_content:
|
|
592
|
+
identity_error = validate_reviewer_identity(content, config)
|
|
593
|
+
if identity_error:
|
|
594
|
+
error = identity_error
|
|
595
|
+
break
|
|
596
|
+
response = content
|
|
597
|
+
break
|
|
598
|
+
prev_content = content
|
|
599
|
+
time.sleep(FILE_STABLE_INTERVAL_SEC)
|
|
600
|
+
|
|
601
|
+
if cancel_event.is_set():
|
|
602
|
+
error = cancel_reason
|
|
603
|
+
break
|
|
604
|
+
|
|
605
|
+
try:
|
|
606
|
+
content2 = response_path.read_text(encoding="utf-8").strip()
|
|
607
|
+
except OSError:
|
|
608
|
+
prev_content = None
|
|
609
|
+
continue
|
|
610
|
+
if content2 == content and content2:
|
|
611
|
+
identity_error = validate_reviewer_identity(content2, config)
|
|
612
|
+
if identity_error:
|
|
613
|
+
error = identity_error
|
|
614
|
+
break
|
|
615
|
+
response = content2
|
|
616
|
+
break
|
|
617
|
+
prev_content = content2
|
|
618
|
+
|
|
619
|
+
if error is None and response is None and not cancel_event.is_set():
|
|
620
|
+
error = f"Timed out after {DEFAULT_TIMEOUT_SEC}s waiting for {response_path}"
|
|
621
|
+
finally:
|
|
622
|
+
if identity_error is not None and response_path.exists():
|
|
623
|
+
# The paste is almost right — only its first line is wrong. Park it under
|
|
624
|
+
# a name the next round will not clear (a retried review_reply reuses this
|
|
625
|
+
# same directory and unlinks response.md), so the text survives to be
|
|
626
|
+
# corrected and re-pasted. A second rejection replaces this file: the
|
|
627
|
+
# newest attempt is the one worth keeping, and rotating copies would be
|
|
628
|
+
# scaffolding for a case nobody wants back.
|
|
629
|
+
response_path.replace(pdir / "response.rejected.md")
|
|
630
|
+
clear_pending_state(thread_id, keep_thread_dir=identity_error is not None)
|
|
631
|
+
|
|
632
|
+
return response, error
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
# --- Unified dispatch ---
|
|
637
|
+
|
|
638
|
+
def do_review(prompt: str, config: dict, thread_id: str, history: list,
|
|
639
|
+
cancel_event: threading.Event, cancel_reason: str) -> tuple[str | None, str | None]:
|
|
640
|
+
if MODE == "file":
|
|
641
|
+
return wait_for_file_response(prompt, config, thread_id, history, cancel_event, cancel_reason)
|
|
642
|
+
return wait_for_browser_response(prompt, config, thread_id, history, cancel_event, cancel_reason)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
# --- MCP tool handlers ---
|
|
646
|
+
|
|
647
|
+
def tool_success(request_id: Any, payload: dict[str, Any]) -> dict[str, Any]:
|
|
648
|
+
return {
|
|
649
|
+
"jsonrpc": "2.0",
|
|
650
|
+
"id": request_id,
|
|
651
|
+
"result": {
|
|
652
|
+
"content": [{"type": "text", "text": json.dumps(payload, ensure_ascii=False)}],
|
|
653
|
+
},
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def tool_error(request_id: Any, message: str) -> dict[str, Any]:
|
|
658
|
+
return {
|
|
659
|
+
"jsonrpc": "2.0",
|
|
660
|
+
"id": request_id,
|
|
661
|
+
"result": {
|
|
662
|
+
"content": [{"type": "text", "text": json.dumps({"error": message}, ensure_ascii=False)}],
|
|
663
|
+
"isError": True,
|
|
664
|
+
},
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def handle_review(args: dict, request_id: Any, cancel_event: threading.Event,
|
|
669
|
+
cancel_reason: str) -> dict[str, Any]:
|
|
670
|
+
prompt = str(args.get("prompt", "")).strip()
|
|
671
|
+
if not prompt:
|
|
672
|
+
return tool_error(request_id, "prompt is required")
|
|
673
|
+
config = args.get("config", {})
|
|
674
|
+
if not isinstance(config, dict):
|
|
675
|
+
config = {}
|
|
676
|
+
|
|
677
|
+
thread_id = create_thread()
|
|
678
|
+
append_exchange(thread_id, "user", prompt)
|
|
679
|
+
|
|
680
|
+
response, error = do_review(prompt, config, thread_id, [], cancel_event, cancel_reason)
|
|
681
|
+
if error:
|
|
682
|
+
return tool_error(request_id, error)
|
|
683
|
+
|
|
684
|
+
append_exchange(thread_id, "assistant", response)
|
|
685
|
+
return tool_success(request_id, {"threadId": thread_id, "content": response})
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def handle_review_reply(args: dict, request_id: Any, cancel_event: threading.Event,
|
|
689
|
+
cancel_reason: str) -> dict[str, Any]:
|
|
690
|
+
thread_id = str(args.get("threadId", "")).strip()
|
|
691
|
+
if not thread_id:
|
|
692
|
+
return tool_error(request_id, "threadId is required")
|
|
693
|
+
if thread_id not in _threads:
|
|
694
|
+
return tool_error(request_id, f"Unknown threadId: {thread_id}")
|
|
695
|
+
|
|
696
|
+
prompt = str(args.get("prompt", "")).strip()
|
|
697
|
+
if not prompt:
|
|
698
|
+
return tool_error(request_id, "prompt is required")
|
|
699
|
+
config = args.get("config", {})
|
|
700
|
+
if not isinstance(config, dict):
|
|
701
|
+
config = {}
|
|
702
|
+
|
|
703
|
+
history = get_history(thread_id)
|
|
704
|
+
append_exchange(thread_id, "user", prompt)
|
|
705
|
+
|
|
706
|
+
response, error = do_review(prompt, config, thread_id, history, cancel_event, cancel_reason)
|
|
707
|
+
if error:
|
|
708
|
+
return tool_error(request_id, error)
|
|
709
|
+
|
|
710
|
+
append_exchange(thread_id, "assistant", response)
|
|
711
|
+
return tool_success(request_id, {"threadId": thread_id, "content": response})
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
# --- MCP request router ---
|
|
715
|
+
|
|
716
|
+
def handle_request(request: dict[str, Any]) -> dict[str, Any] | None:
|
|
717
|
+
request_id = request.get("id")
|
|
718
|
+
method = request.get("method", "")
|
|
719
|
+
params = request.get("params", {})
|
|
720
|
+
|
|
721
|
+
if request_id is None:
|
|
722
|
+
return None
|
|
723
|
+
|
|
724
|
+
if method == "initialize":
|
|
725
|
+
return {
|
|
726
|
+
"jsonrpc": "2.0",
|
|
727
|
+
"id": request_id,
|
|
728
|
+
"result": {
|
|
729
|
+
"protocolVersion": "2024-11-05",
|
|
730
|
+
"capabilities": {"tools": {}},
|
|
731
|
+
"serverInfo": {"name": SERVER_NAME, "version": "0.1.0"},
|
|
732
|
+
},
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if method == "ping":
|
|
736
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": {}}
|
|
737
|
+
|
|
738
|
+
if method in {"resources/list", "resources/templates/list"}:
|
|
739
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": {"resources": []}}
|
|
740
|
+
|
|
741
|
+
if method in {"notifications/initialized", "initialized"}:
|
|
742
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": {}}
|
|
743
|
+
|
|
744
|
+
if method == "tools/list":
|
|
745
|
+
return {
|
|
746
|
+
"jsonrpc": "2.0",
|
|
747
|
+
"id": request_id,
|
|
748
|
+
"result": {
|
|
749
|
+
"tools": [
|
|
750
|
+
{
|
|
751
|
+
"name": "review",
|
|
752
|
+
"description": "Start a new manual review session. Opens a browser page where the user copies the prompt to any AI model and pastes the response back.",
|
|
753
|
+
"inputSchema": {
|
|
754
|
+
"type": "object",
|
|
755
|
+
"properties": {
|
|
756
|
+
"prompt": {"type": "string", "description": "The full review prompt to show the user"},
|
|
757
|
+
"config": {"type": "object", "description": "Config hints (e.g. model_reasoning_effort)"},
|
|
758
|
+
},
|
|
759
|
+
"required": ["prompt"],
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
"name": "review_reply",
|
|
764
|
+
"description": "Continue a review conversation in an existing thread. Shows previous exchanges for context.",
|
|
765
|
+
"inputSchema": {
|
|
766
|
+
"type": "object",
|
|
767
|
+
"properties": {
|
|
768
|
+
"threadId": {"type": "string", "description": "Thread ID from a previous review call"},
|
|
769
|
+
"prompt": {"type": "string", "description": "Follow-up prompt"},
|
|
770
|
+
"config": {"type": "object", "description": "Config hints"},
|
|
771
|
+
},
|
|
772
|
+
"required": ["threadId", "prompt"],
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
},
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if method == "tools/call":
|
|
780
|
+
name = params.get("name", "")
|
|
781
|
+
args = params.get("arguments", {})
|
|
782
|
+
if not isinstance(args, dict):
|
|
783
|
+
return tool_error(request_id, "tool arguments must be an object")
|
|
784
|
+
if name == "review":
|
|
785
|
+
return handle_review(args, request_id, threading.Event(), "")
|
|
786
|
+
if name == "review_reply":
|
|
787
|
+
return handle_review_reply(args, request_id, threading.Event(), "")
|
|
788
|
+
return tool_error(request_id, f"unknown tool: {name}")
|
|
789
|
+
|
|
790
|
+
return {
|
|
791
|
+
"jsonrpc": "2.0",
|
|
792
|
+
"id": request_id,
|
|
793
|
+
"error": {"code": -32601, "message": f"Unknown method: {method}"},
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
# --- Main loop (non-blocking, fail-fast for concurrent calls) ---
|
|
798
|
+
|
|
799
|
+
def _run_blocking_tool(handler, args, request_id, pending: _PendingCall):
|
|
800
|
+
"""Run a blocking tool handler in background, send response when done."""
|
|
801
|
+
global _pending_call
|
|
802
|
+
try:
|
|
803
|
+
response = handler(args, request_id, pending.cancel_event, pending.cancel_reason)
|
|
804
|
+
if not pending.suppress_response and not pending.cancel_event.is_set():
|
|
805
|
+
# Clear _pending_call BEFORE sending so the next request doesn't
|
|
806
|
+
# fail-fast on a thread that's about to exit
|
|
807
|
+
if _pending_call is pending:
|
|
808
|
+
_pending_call = None
|
|
809
|
+
send_response(response)
|
|
810
|
+
except Exception:
|
|
811
|
+
debug_log(f"Unhandled exception in tool handler for request {request_id}")
|
|
812
|
+
if _pending_call is pending:
|
|
813
|
+
_pending_call = None
|
|
814
|
+
if not pending.suppress_response and not pending.cancel_event.is_set():
|
|
815
|
+
send_response(tool_error(request_id, "Internal error in manual review handler"))
|
|
816
|
+
finally:
|
|
817
|
+
if _pending_call is pending:
|
|
818
|
+
_pending_call = None
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def _cancel_active_call(request_id: Any, reason: str) -> None:
|
|
822
|
+
"""Cancel the active pending call. Only sets signals — the worker thread's
|
|
823
|
+
finally block handles server shutdown, port release, and pending state cleanup."""
|
|
824
|
+
global _pending_call, _current_session, _active_server
|
|
825
|
+
if _pending_call is None:
|
|
826
|
+
return
|
|
827
|
+
if _pending_call.request_id != request_id:
|
|
828
|
+
debug_log(f"Ignoring cancel for request {request_id}; active is {_pending_call.request_id}")
|
|
829
|
+
return
|
|
830
|
+
_pending_call.cancel_reason = reason
|
|
831
|
+
_pending_call.suppress_response = True
|
|
832
|
+
_pending_call.cancel_event.set()
|
|
833
|
+
# Unblock browser wait loop
|
|
834
|
+
if _current_session is not None:
|
|
835
|
+
_current_session.done.set()
|
|
836
|
+
# Shut down HTTP server to unblock serve_forever (worker handles server_close)
|
|
837
|
+
with _active_server_lock:
|
|
838
|
+
if _active_server is not None:
|
|
839
|
+
try:
|
|
840
|
+
_active_server.shutdown()
|
|
841
|
+
except Exception:
|
|
842
|
+
pass
|
|
843
|
+
# Worker thread owns cleanup; do not call clear_pending_state or server_close here
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def main() -> int:
|
|
847
|
+
global _pending_call
|
|
848
|
+
_init_stdio()
|
|
849
|
+
debug_log(f"Server starting: mode={MODE}, timeout={DEFAULT_TIMEOUT_SEC}s")
|
|
850
|
+
while True:
|
|
851
|
+
request = read_message()
|
|
852
|
+
if request is None:
|
|
853
|
+
return 0
|
|
854
|
+
|
|
855
|
+
request_id = request.get("id")
|
|
856
|
+
method = request.get("method", "")
|
|
857
|
+
params = request.get("params", {})
|
|
858
|
+
|
|
859
|
+
# Handle cancellation notification (MCP spec)
|
|
860
|
+
if method == "notifications/cancelled":
|
|
861
|
+
try:
|
|
862
|
+
cancelled_request_id = params.get("requestId")
|
|
863
|
+
reason = params.get("reason", "Client cancelled request")
|
|
864
|
+
_cancel_active_call(cancelled_request_id, reason)
|
|
865
|
+
except Exception:
|
|
866
|
+
debug_log("Exception during cancel notification (ignored)")
|
|
867
|
+
continue # notification — no response
|
|
868
|
+
|
|
869
|
+
# For tool calls that block (review, review_reply), run in background
|
|
870
|
+
if method == "tools/call":
|
|
871
|
+
name = params.get("name", "")
|
|
872
|
+
if name in ("review", "review_reply"):
|
|
873
|
+
# Fail-fast: if another review is already active, reject
|
|
874
|
+
if _pending_call is not None:
|
|
875
|
+
if _pending_call.thread is not None and _pending_call.thread.is_alive():
|
|
876
|
+
send_response(tool_error(
|
|
877
|
+
request_id,
|
|
878
|
+
"Another manual review is already in progress. "
|
|
879
|
+
"Finish it in the browser/file response path, "
|
|
880
|
+
"or cancel the previous tool call before starting a new one.",
|
|
881
|
+
))
|
|
882
|
+
continue
|
|
883
|
+
else:
|
|
884
|
+
# Stale reference — clean up
|
|
885
|
+
_pending_call = None
|
|
886
|
+
|
|
887
|
+
args = params.get("arguments", {})
|
|
888
|
+
if not isinstance(args, dict):
|
|
889
|
+
send_response(tool_error(request_id, "tool arguments must be an object"))
|
|
890
|
+
continue
|
|
891
|
+
|
|
892
|
+
pending = _PendingCall(request_id)
|
|
893
|
+
_pending_call = pending
|
|
894
|
+
handler = handle_review if name == "review" else handle_review_reply
|
|
895
|
+
pending.thread = threading.Thread(
|
|
896
|
+
target=_run_blocking_tool,
|
|
897
|
+
args=(handler, args, request_id, pending),
|
|
898
|
+
daemon=True,
|
|
899
|
+
)
|
|
900
|
+
pending.thread.start()
|
|
901
|
+
continue
|
|
902
|
+
|
|
903
|
+
# Non-blocking requests handled synchronously
|
|
904
|
+
response = handle_request(request)
|
|
905
|
+
if response is not None:
|
|
906
|
+
send_response(response)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
if __name__ == "__main__":
|
|
910
|
+
raise SystemExit(main())
|