@tea-agent/loop-agent 0.24.4-beta.0 → 0.24.5
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/AGENTS.md +13 -5
- package/CHANGELOG.md +52 -0
- package/README.md +5 -2
- package/dist/commands/init.js +16 -2
- package/dist/worker/cli.js +59 -14
- package/dist/worker/console/operator-actions.js +135 -24
- package/dist/worker/console/static/assets/index-CSRIhuzh.js +29 -0
- package/dist/worker/console/static/assets/index-Yyn3ynVv.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/prd-file-import.js +218 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +77 -11
- package/dist/workflows/dag/init-hybrid.js +15 -15
- package/dist/workflows/dag/l5-report-metrics.js +0 -3
- package/docs/architecture/evolution.md +1 -1
- package/docs/architecture/system-overview.md +1 -1
- package/docs/operations/local-development-environment.md +19 -0
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -2
- package/docs/templates/backend-test-dag.json +7 -7
- package/docs/templates/backend-test-dag.retrospect.prompt.md +0 -3
- package/docs/templates/backend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +2 -2
- package/harness.json +2 -2
- package/package.json +1 -1
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/loop-agent/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/dist/worker/console/static/assets/index-Cv68Ge5n.js +0 -27
- package/dist/worker/console/static/assets/index-Dp_2sKGk.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--canvas: #f7f7f4;--canvas-soft: #fafaf7;--surface: #ffffff;--surface-muted: #f0efe9;--ink: #26251e;--body: #5a5852;--muted: #807d72;--muted-soft: #a09c92;--hairline: #e6e5e0;--hairline-strong: #cfcdc4;--orange: #f54e00;--orange-active: #d04200;--orange-soft: color-mix(in srgb, var(--orange) 12%, white);--blue: #3f6ea4;--green: #1f8a65;--amber: #a66a00;--red: #c72d53;--peach: #f1d8cb;--mint: #d8ead5;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-8: 32px;--radius-sm: 6px;--radius-md: 8px;--radius-lg: 12px;--radius-pill: 999px;--transition-fast: .16s ease;--transition-base: .22s ease;--shadow-panel: 0 1px 2px rgb(38 37 30 / 4%), 0 8px 24px rgb(38 37 30 / 3%);--shadow-soft: 0 10px 30px rgb(38 37 30 / 6%);--shell-max: 1680px;color-scheme:light;font-family:Segoe UI,PingFang SC,Hiragino Sans GB,Noto Sans SC,system-ui,-apple-system,sans-serif;line-height:1.5}*{box-sizing:border-box}html{background:radial-gradient(1200px 480px at 8% -10%,rgb(241 216 203 / 45%),transparent 55%),radial-gradient(900px 420px at 100% 0%,rgb(216 234 213 / 35%),transparent 50%),var(--canvas);min-height:100%}body{margin:0;min-height:100vh;min-width:320px;background:transparent;color:var(--ink);font-size:14px}button,input,select,textarea{font:inherit}a{color:inherit}code,.cmd,.kv dd code{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace}.embedded-operator-nav{position:sticky;top:0;z-index:40;display:flex;gap:6px;align-items:center;margin:0;padding:8px max(24px,calc((100vw - var(--shell-max)) / 2));border-bottom:1px solid var(--hairline);background:color-mix(in srgb,var(--canvas) 92%,transparent);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);font-size:13px;font-weight:700;flex-shrink:0}.console-app{display:flex;flex-direction:column;min-height:100dvh}html:has(.console-app.is-chat-focus),body:has(.console-app.is-chat-focus){height:100%;overflow:hidden}.console-app.is-chat-focus{height:100dvh;max-height:100dvh;overflow:hidden}.console-shell{flex:1 1 auto;display:flex;flex-direction:column;min-height:0;width:min(100% - 48px,var(--shell-max));margin:0 auto;padding:var(--space-5) 0 var(--space-12, 48px)}.embedded-operator-nav a,.embedded-operator-nav span{padding:6px 12px;border-radius:var(--radius-pill);text-decoration:none;color:var(--ink)}.embedded-operator-nav a:hover,.embedded-operator-nav a:focus-visible{background:var(--surface);color:var(--orange-active);outline:none}.embedded-operator-nav span[aria-current=page]{background:var(--ink);color:#fff}.top-nav{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);margin:0 0 var(--space-5);padding:6px;border:1px solid var(--hairline);border-radius:var(--radius-pill);background:color-mix(in srgb,var(--surface) 88%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:var(--shadow-panel)}.nav-link{appearance:none;display:inline-flex;align-items:center;min-height:34px;padding:0 14px;border:1px solid transparent;border-radius:var(--radius-pill);background:transparent;color:var(--body);font-size:13px;font-weight:600;cursor:pointer;transition:background-color var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast),transform var(--transition-fast)}.nav-link:hover:not(:disabled),.nav-link:focus-visible:not(:disabled){background:var(--surface);color:var(--ink);outline:none}.nav-link-active{background:var(--ink);color:#fff}.nav-link-active:hover:not(:disabled),.nav-link-active:focus-visible:not(:disabled){background:var(--ink);color:#fff}.nav-link-external{margin-left:auto;border-color:var(--hairline);background:var(--surface);color:var(--orange-active)}.nav-link-external:hover:not(:disabled){border-color:var(--orange);background:var(--orange-soft);color:var(--orange-active)}.readiness{appearance:none;position:relative;display:inline-flex;align-items:center;gap:6px;margin-left:auto;min-height:34px;padding:0 28px 0 12px;border:1px solid var(--hairline);border-radius:var(--radius-pill);background:var(--surface);color:var(--ink);cursor:pointer;box-shadow:none;transition:background-color var(--transition-fast),border-color var(--transition-fast)}.readiness:hover{border-color:color-mix(in srgb,var(--ink) 18%,var(--hairline));background:color-mix(in srgb,var(--surface) 70%,var(--bg))}.readiness:focus-visible{outline:2px solid color-mix(in srgb,var(--orange) 45%,transparent);outline-offset:2px}.readiness:before{content:"";position:absolute;top:50%;right:10px;width:7px;height:7px;border-radius:50%;transform:translateY(-50%);background:var(--muted-soft)}.readiness[data-state=setup-required]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=setup-required]:before{background:var(--amber)}.readiness[data-state=ready]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=ready]:before{background:color-mix(in srgb,var(--green) 72%,white);box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 12%,transparent)}.readiness[data-state=degraded]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=degraded]:before{background:var(--amber);box-shadow:0 0 0 3px color-mix(in srgb,var(--amber) 12%,transparent)}.readiness-label{margin:0;color:var(--muted);font-size:11px;font-weight:600;line-height:1;letter-spacing:.02em;text-transform:uppercase}.readiness strong{color:var(--ink);font-size:12px;font-weight:700;line-height:1}.readiness[data-state=ready] strong,.readiness[data-state=setup-required] strong,.readiness[data-state=degraded] strong{color:var(--ink)}.flash{display:flex;gap:var(--space-3);align-items:flex-start;margin:0 0 var(--space-4);padding:12px 14px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel);animation:flash-in .22s ease}.flash-error{border-color:color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 6%,var(--surface));color:var(--red)}.flash-info{border-color:color-mix(in srgb,var(--blue) 30%,var(--hairline));background:color-mix(in srgb,var(--blue) 6%,var(--surface));color:var(--body)}.flash-label{flex:0 0 auto;min-width:3.5rem;font-size:12px;font-weight:700;letter-spacing:.02em}.flash-body{margin:0;overflow-wrap:anywhere}.muted{color:var(--muted)}.console-main{display:grid;gap:var(--space-4)}.console-main--fill{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:var(--space-3)}.console-shell.is-chat-focus{flex:1 1 auto;min-height:0;overflow:hidden;padding-bottom:var(--space-3)}.console-shell.is-chat-focus .top-nav{margin-bottom:var(--space-3);flex-shrink:0}.console-shell.is-chat-focus .console-main--fill{flex:1 1 auto;min-height:0;overflow:hidden}.panel--chat{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;padding:var(--space-3);overflow:hidden}.panel--chat .oc-shell{flex:1 1 auto;min-height:0;height:auto;overflow:hidden}.console-shell.is-chat-focus .panel--chat{padding:var(--space-2)}.console-shell.is-chat-focus .panel--chat .oc-shell{min-height:0;gap:var(--space-2);overflow:hidden}.inspect-split-panel{position:fixed;z-index:50;top:16px;right:16px;bottom:16px;width:min(420px,calc(100vw - 32px));display:grid;align-content:start;gap:var(--space-4);padding:var(--space-5);overflow:auto;border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface);box-shadow:var(--shadow-soft)}.inspect-split-nodes{display:grid;gap:6px;margin:0;padding:0;list-style:none;max-height:220px;overflow:auto}.inspect-split-nodes li{display:grid;grid-template-columns:1fr auto;gap:var(--space-2);align-items:center;padding:6px 8px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--canvas-soft);font-size:12px}.inspect-split-nodes li.is-active{border-color:var(--orange);background:var(--orange-soft)}.inspect-split-nodes code{color:var(--muted);font-size:11px}.inspect-split-header{display:flex;align-items:start;justify-content:space-between;gap:var(--space-3)}.inspect-split-header h2,.inspect-split-panel h3{margin:0}.inspect-split-kicker,.inspect-split-id{margin:0;color:var(--muted);font-size:12px}.inspect-split-facts{display:grid;gap:var(--space-2);margin:0}.inspect-split-facts div{display:grid;grid-template-columns:7rem 1fr;gap:var(--space-2);padding-bottom:var(--space-2);border-bottom:1px solid var(--hairline)}.inspect-split-facts dt{color:var(--muted)}.inspect-split-facts dd{margin:0;overflow-wrap:anywhere}.inspect-split-events{display:grid;gap:8px;margin:0;padding-left:18px;color:var(--body);font-size:12px}.panel{min-width:0;border:1px solid var(--hairline);background:color-mix(in srgb,var(--surface) 96%,transparent);border-radius:var(--radius-lg);padding:var(--space-6);box-shadow:var(--shadow-panel)}.panel h2{margin:0 0 var(--space-2);color:var(--ink);font-size:18px;font-weight:700;letter-spacing:-.01em}.panel-lead{margin:0 0 var(--space-5);color:var(--muted);font-size:13px}.panel h3{margin:0 0 var(--space-3);color:var(--ink);font-size:15px;font-weight:700}.empty-state{border:1px dashed var(--hairline-strong);border-radius:var(--radius-md);padding:var(--space-6);background:var(--canvas-soft);color:var(--body)}.empty-title{margin:0 0 var(--space-2);color:var(--ink);font-size:16px;font-weight:700}.checklist{margin:0 0 var(--space-4);padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--canvas-soft);list-style:none;color:var(--body)}.checklist li{position:relative;padding:6px 0 6px 22px}.checklist li:before{content:"";position:absolute;left:0;top:12px;width:8px;height:8px;border-radius:50%;background:var(--orange);opacity:.75}.kv{display:grid;gap:var(--space-1);margin:0}.kv div{display:grid;grid-template-columns:9rem 1fr;gap:var(--space-3);padding:10px 0;border-bottom:1px solid var(--hairline)}.kv div:last-child{border-bottom:none}.kv dt{color:var(--muted);font-size:12px;font-weight:600}.kv dd{margin:0;color:var(--ink);overflow-wrap:anywhere}code{font-size:.85rem;color:var(--ink)}.cmd{display:block;width:100%;margin:0;padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:#f3efe8;color:var(--ink);font-size:12px;line-height:1.55;overflow-x:auto;white-space:pre-wrap;word-break:break-word;max-height:280px}.external-link{color:var(--orange);text-decoration:none;font-weight:600}.external-link:hover{text-decoration:underline}.error{color:var(--red)}.wizard-steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(108px,1fr));gap:var(--space-2);list-style:none;padding:0;margin:0 0 var(--space-5)}.wizard-steps li{position:relative;min-height:52px;display:grid;align-content:center;gap:2px;border:1px solid var(--hairline);border-radius:var(--radius-md);padding:8px 10px;background:var(--canvas-soft);color:var(--muted);font-size:12px;font-weight:650;transition:border-color var(--transition-fast),background-color var(--transition-fast),color var(--transition-fast),box-shadow var(--transition-fast)}.wizard-steps li[data-done=true]{border-color:color-mix(in srgb,var(--green) 35%,var(--hairline));background:color-mix(in srgb,var(--mint) 45%,var(--surface));color:var(--green)}.wizard-steps li[data-active=true]{border-color:var(--orange);background:var(--surface);color:var(--ink);box-shadow:0 0 0 3px var(--orange-soft)}.wizard-steps .step-index{font-size:10px;font-weight:700;letter-spacing:.04em;color:var(--muted-soft)}.wizard-steps li[data-active=true] .step-index,.wizard-steps li[data-done=true] .step-index{color:inherit;opacity:.8}.wizard-steps .step-label{line-height:1.25}.field{display:grid;gap:var(--space-1);margin:0 0 var(--space-4);width:min(100%,520px)}.field span{font-size:12px;font-weight:600;color:var(--muted)}.field input,.field textarea,.field select{width:100%;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);transition:border-color var(--transition-fast),box-shadow var(--transition-fast)}.field input{min-height:40px;padding:0 var(--space-3)}.field select{min-height:40px;padding:0 var(--space-3);cursor:pointer}.field textarea{min-height:120px;padding:var(--space-3);resize:vertical}.field input:focus-visible,.field textarea:focus-visible,.field select:focus-visible{outline:none;border-color:var(--orange);box-shadow:0 0 0 3px var(--orange-soft)}.field input:disabled,.field textarea:disabled,.field select:disabled{background:var(--surface-muted);color:var(--muted);cursor:not-allowed}.workflow-hint{margin:calc(var(--space-1) * -1) 0 var(--space-3);color:var(--muted);font-size:12px}.prd-drop-zone{position:relative;width:min(100%,720px);margin:0 0 var(--space-3);padding:var(--space-3);border:1px dashed var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);transition:border-color var(--transition-fast),background-color var(--transition-fast),box-shadow var(--transition-fast)}.prd-drop-zone.is-dragover{border-color:var(--orange);background:var(--orange-soft);box-shadow:0 0 0 3px var(--orange-soft)}.prd-upload-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2) var(--space-3);margin:0 0 var(--space-3)}.prd-upload-btn{display:inline-flex;align-items:center;cursor:pointer;margin:0}.prd-upload-btn.is-disabled,.prd-upload-btn:has(input:disabled){opacity:.55;cursor:not-allowed;pointer-events:none}.prd-upload-btn:focus-within{outline:none;box-shadow:0 0 0 3px var(--orange-soft)}.prd-upload-hint{font-size:12px}.prd-file-meta{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2) var(--space-3);margin:0 0 var(--space-3);padding:var(--space-2) var(--space-3);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface-muted);font-size:12px;color:var(--ink)}.prd-file-list{list-style:none;margin:0 0 var(--space-3);padding:0;display:grid;gap:var(--space-2)}.prd-file-list li{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--space-2);padding:var(--space-2) var(--space-3);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface-muted);font-size:12px}.prd-file-list li.is-primary{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline-strong))}.prd-file-item-name{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--space-1);min-width:0}.prd-file-primary-tag{display:inline-flex;align-items:center;padding:0 6px;border-radius:999px;background:var(--orange-soft);color:var(--orange);font-size:11px;font-weight:700}.prd-file-item-actions{display:inline-flex;flex-wrap:wrap;gap:var(--space-1)}.prd-file-item-actions .btn,.prd-file-clear{min-height:28px;padding:0 var(--space-2);font-size:12px}.prd-drop-hint{margin:0 0 var(--space-3);font-size:13px;font-weight:600;color:var(--orange)}.prd-text-field{width:100%;margin-bottom:0}.prd-file-error{width:min(100%,720px);margin:0 0 var(--space-3);padding:var(--space-2) var(--space-3);border:1px solid color-mix(in srgb,var(--orange) 45%,var(--hairline-strong));border-radius:var(--radius-md);background:var(--orange-soft);color:var(--ink);font-size:13px}.meta-row{display:grid;gap:6px;margin:0 0 var(--space-4);padding:var(--space-3) var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--canvas-soft);font-size:13px}.meta-row code{font-size:12px}.cta-row,.recovery-cta-row{display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:center}.cta-row{margin-top:var(--space-3)}.cta-row .cmd{flex:1 1 100%}.wizard-body{display:grid;gap:var(--space-3);justify-items:stretch;animation:panel-in .24s ease}.wizard-body>div{display:grid;gap:var(--space-3);justify-items:stretch}.wizard-body .field{width:100%;max-width:640px}.wizard-body .cta-row{margin-top:var(--space-1)}.interview-card{width:100%;padding:var(--space-5);border:1px solid var(--hairline);border-radius:var(--radius-lg);background:linear-gradient(180deg,var(--surface),var(--canvas-soft));box-shadow:var(--shadow-panel)}.interview-q{margin:0 0 var(--space-2);color:var(--ink)}.btn,button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:40px;padding:0 16px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);font-size:13px;font-weight:650;cursor:pointer;transition:border-color var(--transition-fast),background-color var(--transition-fast),color var(--transition-fast),box-shadow var(--transition-fast),transform var(--transition-fast)}.btn:hover:not(:disabled),button:hover:not(:disabled){border-color:color-mix(in srgb,var(--ink) 18%,var(--hairline-strong));background:var(--canvas-soft);outline:none}.btn:focus-visible:not(:disabled),button:focus-visible:not(:disabled){outline:none;box-shadow:0 0 0 3px var(--orange-soft)}.btn:active:not(:disabled),button:active:not(:disabled){transform:translateY(1px)}.btn:disabled,button:disabled{opacity:.48;cursor:not-allowed;transform:none}.btn-primary,button.btn-primary{border-color:var(--orange);background:var(--orange);color:#fff;box-shadow:0 6px 16px #f54e002e}.btn-primary:hover:not(:disabled),button.btn-primary:hover:not(:disabled){border-color:var(--orange-active);background:var(--orange-active);color:#fff}.btn-secondary,button.btn-secondary{border-color:var(--hairline-strong);background:var(--surface);color:var(--ink)}.btn-ghost,button.btn-ghost{border-color:transparent;background:transparent;color:var(--body)}.btn-ghost:hover:not(:disabled),button.btn-ghost:hover:not(:disabled){background:var(--surface-muted);color:var(--ink)}.recovery-cta-row{margin-top:var(--space-3)}.recovery-confirm{margin-top:var(--space-3);padding:var(--space-4);border:1px solid color-mix(in srgb,var(--orange) 40%,var(--hairline));border-radius:var(--radius-md);background:color-mix(in srgb,var(--orange-soft) 55%,var(--surface))}.recovery-confirm h3{margin:0 0 var(--space-2);font-size:15px}.recovery-confirm .cta-row{margin-top:var(--space-3)}.recovery-report{margin-top:var(--space-3);padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface)}.recovery-report-header{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);margin-bottom:var(--space-3)}.recovery-report-header h3{margin:0;font-size:15px}.recovery-report-facts{display:grid;gap:var(--space-2);margin:0 0 var(--space-3)}.recovery-report-facts div{display:grid;grid-template-columns:5.5rem 1fr;gap:var(--space-2)}.recovery-report-facts dt{color:var(--muted);font-size:12px}.recovery-report-facts dd{margin:0;overflow-wrap:anywhere}.recovery-steps{margin:.5rem 0 .75rem 1.25rem;padding:0;line-height:1.5}.recovery-lane{margin-top:var(--space-4);padding-top:var(--space-3);border-top:1px solid var(--border, #ddd)}.recovery-lane h3{margin:0 0 .35rem;font-size:1rem}.recovery-cta-row button[disabled]{opacity:.4}@keyframes flash-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}@keyframes panel-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}@media(max-width:800px){.console-shell{width:min(100% - 24px,var(--shell-max));padding-top:var(--space-5)}.readiness,.nav-link-external{margin-left:0}.inspect-split-panel{inset:auto 0 0;width:100%;max-height:min(76vh,680px);border-radius:var(--radius-lg) var(--radius-lg) 0 0}.kv div{grid-template-columns:1fr}.wizard-steps{grid-template-columns:repeat(2,minmax(0,1fr))}}.oc-shell{display:flex;flex-direction:column;height:100%;min-height:0;gap:var(--space-3);position:relative;overflow:hidden}.oc-workspace{display:flex;flex:1 1 auto;min-height:0;gap:0;position:relative;overflow:hidden}.oc-main{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;min-height:0;overflow:hidden}.oc-sidebar{flex:0 0 280px;width:280px;min-width:0;display:flex;flex-direction:column;gap:var(--space-2);margin-right:var(--space-3);padding:var(--space-2);overflow:hidden;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface)}.oc-sidebar.is-collapsed{flex:0 0 36px;width:36px;padding:6px;overflow:visible;align-items:center}.oc-sidebar-header-actions{display:inline-flex;align-items:center;gap:2px}.oc-sidebar-toggle{appearance:none;display:inline-grid;place-items:center;width:28px;height:28px;padding:0;border:1px solid transparent;border-radius:var(--radius-sm);background:transparent;color:var(--muted);cursor:pointer;transition:background-color var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast)}.oc-sidebar-toggle:hover{color:var(--ink);background:color-mix(in srgb,var(--orange) 8%,var(--surface));border-color:var(--hairline)}.oc-sidebar-toggle:focus-visible{outline:2px solid color-mix(in srgb,var(--orange) 45%,transparent);outline-offset:1px}.oc-sidebar-toggle.is-collapsed{width:100%;height:28px}.oc-sidebar-section{display:flex;flex-direction:column;gap:6px;min-height:0}.oc-sidebar-section>header{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:13px;flex-shrink:0}.oc-sessions{flex:0 1 auto;max-height:min(34vh,320px)}.oc-session-scroll{min-height:0;max-height:min(28vh,260px);overflow:auto;padding:4px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:linear-gradient(180deg,color-mix(in srgb,var(--canvas-soft) 70%,white),var(--canvas-soft))}.oc-session-group{display:flex;flex-direction:column;gap:2px}.oc-session-group>small{margin:4px 6px 2px;color:var(--muted-soft);font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.oc-session-item{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:2px;align-items:center;border-radius:6px;transition:background-color var(--transition-fast)}.oc-session-item:hover,.oc-session-item:focus-within{background:color-mix(in srgb,var(--orange) 8%,var(--surface))}.oc-session-item.is-active{background:var(--orange-soft)}.oc-session-main{min-width:0;padding:5px 6px;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer}.oc-session-main strong,.oc-session-main span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.oc-session-main strong{font-size:12.5px;font-weight:650;line-height:1.25}.oc-session-main span{color:var(--muted-soft);font-size:10px;margin-top:1px;font-variant-numeric:tabular-nums}.oc-session-actions{display:none;align-items:center;gap:0;padding-right:2px}.oc-session-item:hover .oc-session-actions,.oc-session-item:focus-within .oc-session-actions,.oc-session-item.is-active .oc-session-actions{display:inline-flex}.oc-session-menu{padding:3px 4px;border:0;background:transparent;color:var(--muted);cursor:pointer;font:inherit;font-size:10px;white-space:nowrap}.oc-session-menu:hover{color:var(--ink)}.oc-session-delete{color:color-mix(in srgb,var(--red) 75%,var(--muted))}.oc-session-delete:hover{color:var(--red)}.oc-explorer{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--hairline);padding-top:var(--space-3);margin-top:2px}.oc-explorer-hint{margin:0 2px;color:var(--muted-soft);font-size:10.5px;letter-spacing:.01em}.oc-tree-scroll{flex:1 1 auto;min-height:120px;max-height:min(52vh,560px);overflow:auto;padding:4px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:linear-gradient(180deg,color-mix(in srgb,var(--canvas-soft) 70%,white),var(--canvas-soft))}.oc-tree,.oc-tree ul{list-style:none;padding:0;margin:0}.oc-tree-node{position:relative}.oc-tree-row{appearance:none;display:grid;grid-template-columns:12px 14px minmax(0,1fr);align-items:center;gap:6px;width:100%;min-height:28px;margin:0;padding:3px 8px;border:0;border-radius:6px;background:transparent;color:var(--ink);text-align:left;cursor:pointer;transition:background-color var(--transition-fast),color var(--transition-fast)}.oc-tree-row:hover{background:color-mix(in srgb,var(--orange) 8%,var(--surface))}.oc-tree-row:focus-visible{outline:2px solid color-mix(in srgb,var(--orange) 45%,transparent);outline-offset:1px}.oc-tree-chevron{color:var(--muted-soft);transition:transform var(--transition-fast);transform:rotate(0)}.oc-tree-chevron.is-open{transform:rotate(90deg)}.oc-tree-chevron-spacer{width:12px;height:12px}.oc-tree-glyph{flex-shrink:0;color:var(--amber)}.oc-tree-glyph.is-folder-open{color:color-mix(in srgb,var(--orange) 70%,var(--amber))}.oc-tree-glyph.is-file{color:var(--muted)}.oc-tree-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;line-height:1.25;font-variant-numeric:tabular-nums}.oc-tree-dir .oc-tree-label{font-weight:650;color:var(--ink)}.oc-tree-file .oc-tree-label{font-family:JetBrains Mono,SFMono-Regular,Consolas,Segoe UI Mono,monospace;font-size:11.5px;font-weight:500;color:var(--body)}.oc-tree-node.is-dot .oc-tree-label{color:var(--muted-soft);font-weight:500}.oc-tree-node.is-dot .oc-tree-glyph{opacity:.7}.oc-tree-children{position:relative;margin:0}.oc-tree-children:before{content:"";position:absolute;top:0;bottom:2px;left:var(--oc-tree-guide, 14px);width:1px;background:color-mix(in srgb,var(--hairline-strong) 55%,transparent);pointer-events:none}.oc-tree-empty,.oc-sidebar-error{margin:8px 6px;color:var(--muted);font-size:12px;line-height:1.45}.oc-sidebar-error{color:var(--red)}.oc-governance{margin-top:auto;border-top:1px solid var(--hairline);padding-top:8px}.oc-governance-links{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:4px}.oc-governance-links button{min-height:28px;padding:4px 6px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--surface);color:var(--muted);cursor:pointer;font:inherit;font-size:11px;font-weight:600;text-align:center}.oc-governance-links button:hover{color:var(--ink);border-color:color-mix(in srgb,var(--ink) 18%,var(--hairline))}.oc-governance-panel button{border:0;background:transparent;color:var(--muted);cursor:pointer;font:inherit;font-size:11px}.oc-governance-panel{position:absolute;z-index:10;left:282px;bottom:0;width:min(440px,calc(100vw - 320px));max-height:75%;overflow:auto;padding:12px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel)}.oc-governance-panel header,.oc-governance-panel article,.oc-governance-panel label{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:5px 0}.oc-governance-panel small,.oc-governance-panel span{color:var(--muted);font-size:11px}.oc-governance-panel button:disabled{cursor:wait;opacity:.5}.oc-governance-error{color:var(--danger);font-size:12px}.oc-model-provider{display:block!important;border-bottom:1px solid var(--hairline)}.oc-model-provider>div,.oc-model-row,.oc-model-row>div{display:flex;align-items:center;justify-content:space-between;gap:8px}.oc-model-row{padding:4px 0 4px 10px}.oc-model-form{display:grid;gap:6px;margin-top:10px}.oc-model-form input{min-width:0;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--surface);color:var(--ink);padding:6px 8px;font:inherit;font-size:12px}.oc-model-form button{justify-self:start}.oc-model-test-result{padding:6px 8px;border-radius:var(--radius-sm);background:var(--surface-muted);font-size:12px}.oc-file-preview{position:absolute;z-index:4;inset:0;display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel)}.oc-file-preview header{display:flex;align-items:center;gap:var(--space-2);padding:10px 12px;border-bottom:1px solid var(--hairline)}.oc-file-tabs{display:flex;min-width:0;gap:2px;overflow-x:auto}.oc-file-tabs button{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:0;border-radius:var(--radius-sm);background:transparent;color:var(--muted);padding:4px 7px;cursor:pointer}.oc-file-tabs button.is-active{background:var(--surface-muted);color:var(--ink);font-weight:650}.oc-file-preview header span{flex:1;color:var(--muted);font-size:11px}.oc-file-preview pre{flex:1;min-height:0;margin:0;padding:var(--space-3);overflow:auto;font-size:12px;line-height:1.5;white-space:pre-wrap}.oc-file-preview-loading{position:absolute;z-index:5;right:12px;top:12px;padding:6px 8px;border-radius:var(--radius-sm);background:var(--surface-muted);color:var(--muted);font-size:12px}.oc-process{display:flex;flex-direction:column;flex:0 0 min(360px,42%);width:min(360px,42%);min-width:280px;min-height:0;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel);margin-left:var(--space-3);overflow:hidden;animation:panel-in .18s ease}.oc-process-head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:10px 12px;border-bottom:1px solid var(--hairline);flex-shrink:0}.oc-process-title{display:inline-flex;align-items:center;gap:8px;font-weight:700;font-size:13px;color:var(--ink)}.oc-process-title-mark{width:8px;height:8px;border-radius:50%;background:var(--orange)}.oc-process-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:18px;padding:0 6px;border-radius:var(--radius-pill);background:var(--surface-muted);color:var(--muted);font-size:11px;font-weight:650}.oc-process-close{font-size:12px}.oc-process-timeline{flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--space-3);display:flex;flex-direction:column;gap:0}.oc-process-empty{padding:var(--space-4);color:var(--muted);font-size:12.5px;line-height:1.5;text-align:center}.oc-process-item{display:grid;grid-template-columns:20px 52px minmax(0,1fr);gap:8px;align-items:start;padding:4px 0}.oc-process-rail{position:relative;display:grid;place-items:center;min-height:34px}.oc-process-rail:before{position:absolute;top:0;bottom:0;width:1px;background:var(--hairline);content:""}.oc-process-item.is-first .oc-process-rail:before{top:50%}.oc-process-item.is-last .oc-process-rail:before{bottom:50%}.oc-process-dot{position:relative;z-index:1;width:8px;height:8px;border-radius:50%;background:var(--surface);border:2px solid color-mix(in srgb,var(--orange) 55%,var(--hairline))}.oc-process-item.is-running .oc-process-dot{border-color:var(--orange-active);background:var(--orange-soft)}.oc-process-item.is-error .oc-process-dot{border-color:var(--red);background:color-mix(in srgb,var(--red) 18%,var(--surface))}.oc-process-time{padding-top:10px;font-size:11px;color:var(--muted-soft);font-variant-numeric:tabular-nums;white-space:nowrap}.oc-process-body{min-width:0}.oc-process-toggle{cursor:pointer;font:inherit}.oc-process-toggle.is-active{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));background:color-mix(in srgb,var(--orange) 10%,var(--surface));color:var(--orange-active)}.oc-process-toggle.is-running{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));color:var(--orange-active)}.oc-process-toggle.is-error{border-color:color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 8%,var(--surface));color:var(--red)}.oc-process-summary{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border:1px solid var(--hairline);border-radius:var(--radius-pill);background:var(--surface);color:var(--muted);font-size:11.5px;font-weight:650;cursor:pointer;font:inherit;max-width:min(420px,100%);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.oc-process-summary:hover,.oc-process-summary.is-open{border-color:color-mix(in srgb,var(--orange) 35%,var(--hairline));color:var(--orange-active)}.oc-process-summary.is-running{color:var(--orange-active);border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));background:var(--orange-soft)}.oc-process-summary.is-error{color:var(--red);border-color:color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 8%,var(--surface))}.oc-process-summary.is-ok{color:var(--green);border-color:color-mix(in srgb,var(--green) 30%,var(--hairline))}@media(max-width:900px){.oc-sidebar{position:absolute;inset:0 auto 0 0;z-index:7;width:min(300px,calc(100% - 24px));margin-right:0;box-shadow:var(--shadow-soft)}.oc-sidebar.is-collapsed{position:static;width:auto}.oc-workspace.has-process .oc-process{position:absolute;top:0;right:0;bottom:0;z-index:8;width:min(360px,calc(100% - 24px));min-width:0;flex:none;margin-left:0;border-radius:var(--radius-md) 0 0 var(--radius-md);box-shadow:var(--shadow-soft)}}.oc-banner{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2) var(--space-3);padding:10px 14px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:color-mix(in srgb,var(--surface) 94%,transparent);box-shadow:var(--shadow-panel)}.oc-shell>.oc-alert:first-child{margin-top:0}.oc-banner-title{display:inline-flex;align-items:center;gap:6px;font-weight:700;color:var(--ink)}.oc-banner-dot{width:8px;height:8px;border-radius:50%;background:var(--orange)}.oc-banner-body{color:var(--body);font-size:12.5px;line-height:1.45}.oc-banner-body strong{color:var(--ink)}.oc-cap-chips{display:inline-flex;flex-wrap:wrap;gap:6px;margin-left:auto;align-items:center}.oc-cap-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border:1px solid color-mix(in srgb,var(--green) 30%,var(--hairline));border-radius:var(--radius-pill);background:color-mix(in srgb,var(--mint) 50%,var(--surface));color:var(--green);font-size:11.5px;font-weight:650}button.oc-cap-chip{font:inherit;cursor:pointer}.oc-tools-menu{position:relative}.oc-tools-menu--composer .oc-tools-catalog{top:auto;bottom:calc(100% + 8px)}.oc-tools-toggle.is-active{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));background:color-mix(in srgb,var(--orange) 10%,var(--surface));color:var(--orange-active)}.oc-tools-catalog{position:absolute;top:calc(100% + 8px);right:0;z-index:20;width:min(420px,calc(100vw - 48px));max-height:min(480px,60vh);display:flex;flex-direction:column;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-soft);animation:panel-in .16s ease;overflow:hidden}.oc-tools-catalog-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--hairline);flex-shrink:0}.oc-tools-catalog-head strong{font-size:13px;color:var(--ink)}.oc-tools-catalog-meta{font-size:11.5px;color:var(--muted-soft)}.oc-tools-catalog-close{margin-left:auto;font-size:12px}.oc-tools-catalog-body{overflow-y:auto;padding:var(--space-3);display:flex;flex-direction:column;gap:var(--space-3)}.oc-tools-group-title{margin:0 0 8px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted-soft)}.oc-tools-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.oc-tools-item{padding:8px 10px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:color-mix(in srgb,var(--surface-muted) 55%,var(--surface))}.oc-tools-item-head{display:flex;align-items:center;gap:8px;margin-bottom:4px}.oc-tools-name{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12.5px;font-weight:650;color:var(--ink)}.oc-tools-kind{margin-left:auto;font-size:10.5px;font-weight:650;padding:1px 7px;border-radius:var(--radius-pill);color:var(--muted);background:var(--surface);border:1px solid var(--hairline)}.oc-tools-kind.is-explore,.oc-tools-kind.is-read{color:var(--green);border-color:color-mix(in srgb,var(--green) 28%,var(--hairline));background:color-mix(in srgb,var(--mint) 45%,transparent)}.oc-tools-kind.is-mutation{color:var(--orange-active);border-color:color-mix(in srgb,var(--orange) 35%,var(--hairline));background:var(--orange-soft)}.oc-tools-kind.is-long-running{color:var(--amber);border-color:color-mix(in srgb,var(--amber) 35%,var(--hairline));background:color-mix(in srgb,var(--amber) 10%,var(--surface))}.oc-tools-desc{margin:0;font-size:12px;line-height:1.45;color:var(--body)}.oc-tools-cli{margin:4px 0 0;font-size:11px;color:var(--muted-soft)}.oc-tools-cli code{font-size:11px;color:var(--muted)}.oc-alert{padding:8px 12px;border-radius:var(--radius-md);font-size:12.5px;line-height:1.45;animation:panel-in .18s ease}.oc-alert-warning{border:1px solid color-mix(in srgb,var(--amber) 35%,var(--hairline));background:color-mix(in srgb,var(--amber) 8%,var(--surface));color:var(--amber)}.oc-alert-error{border:1px solid color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 7%,var(--surface));color:var(--red)}.oc-messages{flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--space-3) var(--space-3) var(--space-2);display:flex;flex-direction:column;gap:var(--space-3)}.oc-messages.has-thread:before{content:"";flex:1 1 auto;min-height:0}.oc-messages.is-empty{justify-content:center;align-items:center;padding:var(--space-5) var(--space-4) var(--space-4);background:radial-gradient(640px 300px at 50% 28%,rgb(241 216 203 / 34%),transparent 68%),radial-gradient(520px 240px at 78% 78%,rgb(216 234 213 / 26%),transparent 62%)}.oc-main.is-landing{position:relative}.oc-day-sep{display:flex;align-items:center;gap:var(--space-3);color:var(--muted-soft);font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;margin:var(--space-2) 0}.oc-day-sep:before,.oc-day-sep:after{content:"";flex:1;height:1px;background:var(--hairline)}.oc-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:0 1 auto;gap:var(--space-5);text-align:center;width:min(100%,680px);padding:var(--space-2) var(--space-2) var(--space-4);animation:oc-empty-in .28s ease both}@keyframes oc-empty-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.oc-empty{animation:none}}.oc-empty-hero{display:flex;flex-direction:column;align-items:center;gap:var(--space-3)}.oc-empty-icon{display:grid;place-items:center;width:56px;height:56px;border-radius:50%;background:linear-gradient(145deg,color-mix(in srgb,var(--orange) 18%,white),color-mix(in srgb,var(--mint) 35%,white));color:var(--orange-active);font-size:26px;font-weight:800;font-family:var(--font-mono, "JetBrains Mono", monospace);box-shadow:0 0 0 1px color-mix(in srgb,var(--orange) 18%,transparent)}.oc-empty-copy{display:flex;flex-direction:column;align-items:center;gap:var(--space-2)}.oc-empty h3{margin:0;font-size:20px;font-weight:700;letter-spacing:-.02em;color:var(--ink)}.oc-empty p{margin:0;max-width:48ch;color:var(--muted);font-size:13px;line-height:1.55}.oc-suggestions{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2);width:100%}@media(max-width:560px){.oc-suggestions{grid-template-columns:1fr}}.oc-suggestion{appearance:none;display:flex;flex-direction:column;gap:4px;text-align:left;padding:12px 14px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:color-mix(in srgb,var(--surface) 88%,transparent);color:var(--ink);cursor:pointer;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);transition:border-color var(--transition-fast),background var(--transition-fast),transform var(--transition-fast),box-shadow var(--transition-fast)}.oc-suggestion:hover:not(:disabled){border-color:var(--orange);background:var(--surface);box-shadow:0 0 0 3px var(--orange-soft);transform:translateY(-1px)}.oc-suggestion:disabled{opacity:.55;cursor:not-allowed}.oc-suggestion strong{font-size:13px;font-weight:700}.oc-suggestion span{color:var(--muted);font-size:12px;line-height:1.4}.oc-empty-tips{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--space-2) var(--space-3);margin:0;padding:0;list-style:none;color:var(--muted-soft);font-size:12px}.oc-empty-tips li{display:inline-flex;align-items:center;gap:6px}.oc-empty-tips kbd{display:inline-grid;place-items:center;min-width:1.4em;padding:1px 6px;border:1px solid var(--hairline);border-radius:5px;background:var(--surface);color:var(--body);font-family:var(--font-mono, "JetBrains Mono", monospace);font-size:11px;font-weight:600;line-height:1.4}.oc-empty-cta{margin-top:calc(var(--space-1) * -1)}.oc-row{display:flex;flex-direction:column;gap:4px;max-width:100%}.oc-row.is-user{align-self:flex-end;align-items:flex-end;width:auto;max-width:min(100%,720px)}.oc-row.is-assistant,.oc-row.is-tool{align-self:stretch;align-items:stretch;width:100%;max-width:100%}.oc-meta{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--muted-soft)}.oc-row.is-assistant .oc-meta,.oc-row.is-user .oc-meta{width:100%;max-width:100%}.oc-message-copy{appearance:none;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:26px;height:26px;margin-left:auto;padding:0;border:1px solid transparent;border-radius:var(--radius-sm);background:transparent;color:var(--muted-soft);cursor:pointer;opacity:0;transition:opacity var(--transition-fast),background var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast)}.oc-row:hover .oc-message-copy,.oc-row:focus-within .oc-message-copy,.oc-message-copy:focus-visible{opacity:1}.oc-message-copy:hover{color:var(--body);background:color-mix(in srgb,var(--ink) 6%,transparent);border-color:var(--hairline)}.oc-message-copy.is-copied{opacity:1;color:var(--green, #1a7f37);border-color:color-mix(in srgb,var(--green, #1a7f37) 35%,transparent)}@media(hover:none){.oc-message-copy{opacity:.5}}.oc-avatar{display:grid;place-items:center;width:22px;height:22px;border-radius:50%;font-size:11px;font-weight:700;flex-shrink:0}.oc-avatar-user{background:var(--ink);color:#fff}.oc-avatar-assistant{background:var(--orange);color:#fff}.oc-role{font-weight:700;color:var(--body)}.oc-time{font-variant-numeric:tabular-nums}.oc-bubble{padding:10px 14px;border-radius:var(--radius-lg);font-size:14px;line-height:1.6;overflow-wrap:anywhere;word-break:break-word;animation:bubble-in .16s ease}@keyframes bubble-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.oc-bubble-user{background:var(--ink);color:#fff;border-bottom-right-radius:4px}.oc-bubble-assistant{background:var(--surface);color:var(--ink);border:1px solid var(--hairline);border-bottom-left-radius:4px;box-shadow:var(--shadow-panel)}.oc-bubble-tool{background:var(--canvas-soft);color:var(--body);border:1px solid var(--hairline)}.oc-bubble-error{border-color:color-mix(in srgb,var(--red) 40%,var(--hairline));background:color-mix(in srgb,var(--red) 7%,var(--surface));color:var(--red)}.oc-md{display:flex;flex-direction:column;gap:var(--space-2)}.oc-md>*:first-child{margin-top:0}.oc-md>*:last-child{margin-bottom:0}.oc-md p{margin:0}.oc-md h1,.oc-md h2,.oc-md h3,.oc-md h4{margin:var(--space-3) 0 var(--space-2);color:inherit;font-weight:700;line-height:1.3}.oc-md h1{font-size:1.3em}.oc-md h2{font-size:1.18em}.oc-md h3{font-size:1.06em}.oc-md h4{font-size:1em}.oc-md ul,.oc-md ol{margin:0;padding-left:1.4em;display:flex;flex-direction:column;gap:2px}.oc-md li{margin:0}.oc-md blockquote{margin:0;padding:2px 0 2px 12px;border-left:3px solid var(--hairline-strong);color:var(--body)}.oc-bubble-user .oc-md blockquote{border-left-color:#ffffff59;color:#ffffffd1}.oc-md code{font-size:.86em;padding:1px 5px;border-radius:4px;background:color-mix(in srgb,var(--ink) 8%,transparent)}.oc-bubble-user .oc-md code{background:#ffffff29}.oc-md pre{margin:0;padding:0;background:transparent}.oc-codeblock{margin:0;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface-muted);overflow:hidden}.oc-bubble-assistant .oc-codeblock{background:#f3efe8}.oc-codeblock-head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:5px 6px 5px 10px;border-bottom:1px solid var(--hairline);font-size:11px;color:var(--muted);background:color-mix(in srgb,var(--ink) 4%,transparent)}.oc-codeblock-lang{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-weight:600;text-transform:lowercase}.oc-codeblock-copy{appearance:none;display:inline-flex;align-items:center;gap:5px;min-height:26px;padding:0 9px;border:1px solid transparent;border-radius:var(--radius-sm);background:transparent;color:var(--muted);font-size:11px;font-weight:600;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast)}.oc-codeblock-copy:hover{background:var(--surface);color:var(--ink);border-color:var(--hairline-strong)}.oc-codeblock pre{margin:0;padding:var(--space-3) var(--space-4);overflow-x:auto;font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12.5px;line-height:1.55;white-space:pre;color:var(--ink)}.oc-md a{color:var(--orange);text-decoration:none;border-bottom:1px solid color-mix(in srgb,var(--orange) 40%,transparent)}.oc-md a:hover{border-bottom-color:var(--orange)}.oc-md hr{margin:var(--space-2) 0;border:none;border-top:1px solid var(--hairline-strong)}.oc-stream{white-space:pre-wrap;font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:13px;line-height:1.55}.oc-cursor{display:inline-block;width:7px;height:1.05em;margin-left:2px;vertical-align:text-bottom;background:var(--orange);animation:oc-blink 1s steps(2,start) infinite;border-radius:1px}@keyframes oc-blink{to{opacity:0}}.oc-tool{border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);overflow:hidden;box-shadow:var(--shadow-panel);animation:panel-in .16s ease}.oc-tool.is-error{border-color:color-mix(in srgb,var(--red) 38%,var(--hairline))}.oc-tool.is-running{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline))}.oc-tool-head{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:9px 12px;border:none;background:transparent;text-align:left;cursor:pointer;font:inherit;color:var(--ink)}.oc-tool-head:hover{background:var(--surface-muted)}.oc-tool-chevron{flex-shrink:0;width:12px;height:12px;transition:transform var(--transition-fast)}.oc-tool-head[aria-expanded=true] .oc-tool-chevron{transform:rotate(90deg)}.oc-tool-icon{display:grid;place-items:center;width:22px;height:22px;border-radius:6px;background:var(--surface-muted);color:var(--orange-active);flex-shrink:0}.oc-tool-name{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12.5px;font-weight:650;flex-shrink:0}.oc-tool-args{color:var(--muted);font-size:11.5px;font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;min-width:0}.oc-tool-status{margin-left:auto;flex-shrink:0;font-size:11px;font-weight:650;padding:2px 8px;border-radius:var(--radius-pill)}.oc-tool-waiting{color:var(--muted);font-size:12px;animation:pulse 1.5s infinite}.oc-tool-status.is-running{color:var(--orange-active);background:var(--orange-soft)}.oc-tool-status.is-ok{color:var(--green);background:color-mix(in srgb,var(--mint) 60%,transparent)}.oc-tool-status.is-error{color:var(--red);background:color-mix(in srgb,var(--red) 10%,transparent)}.oc-tool-body{padding:0 var(--space-4) var(--space-3);border-top:1px solid var(--hairline)}.oc-tool-section{display:flex;flex-direction:column;gap:4px;padding-top:var(--space-3)}.oc-tool-label{font-size:11px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--muted-soft)}.oc-tool-code{margin:0;padding:var(--space-3);border-radius:var(--radius-sm);background:var(--surface-muted);font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:260px;overflow-y:auto;color:var(--body)}.oc-composer{flex:0 0 auto;flex-shrink:0;border-top:1px solid var(--hairline);padding:var(--space-2) var(--space-3) var(--space-3);background:color-mix(in srgb,var(--canvas) 92%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);z-index:2}.oc-composer-inner{display:flex;flex-direction:column;gap:var(--space-2);max-width:1080px;margin:0 auto;position:relative}.console-shell.is-chat-focus .oc-composer-inner{max-width:none}.oc-composer-shell{position:relative;display:flex;align-items:flex-end;gap:var(--space-2);padding:8px 8px 8px 14px;border:1px solid var(--hairline-strong);border-radius:var(--radius-lg);background:var(--surface);box-shadow:var(--shadow-panel);transition:border-color var(--transition-fast),box-shadow var(--transition-fast)}.oc-composer-shell:focus-within{border-color:var(--orange);box-shadow:0 0 0 3px var(--orange-soft)}.oc-composer-shell.is-busy{opacity:.78}.oc-textarea{flex:1 1 auto;min-width:0;min-height:52px;max-height:280px;padding:8px 0;border:none;background:transparent;color:var(--ink);font:inherit;font-size:14px;line-height:1.5;resize:none;outline:none}.oc-textarea::placeholder{color:var(--muted-soft)}.oc-send{appearance:none;display:grid;place-items:center;width:38px;height:38px;flex-shrink:0;padding:0;border:1px solid var(--orange);border-radius:50%;background:var(--orange);color:#fff;cursor:pointer;transition:background var(--transition-fast),border-color var(--transition-fast),transform var(--transition-fast)}.oc-send:hover:not(:disabled){background:var(--orange-active);border-color:var(--orange-active)}.oc-send:active:not(:disabled){transform:scale(.94)}.oc-send:disabled{opacity:.4;cursor:not-allowed}.oc-stop{appearance:none;display:grid;place-items:center;width:38px;height:38px;flex-shrink:0;padding:0;border:1px solid var(--hairline-strong);border-radius:50%;background:var(--surface);color:var(--red);cursor:pointer;transition:background var(--transition-fast),border-color var(--transition-fast)}.oc-stop:hover{background:color-mix(in srgb,var(--red) 8%,var(--surface));border-color:var(--red)}.oc-composer-foot{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;font-size:11.5px;color:var(--muted)}.oc-composer-hint{display:inline-flex;align-items:center;gap:5px}.oc-kbd{display:inline-block;padding:1px 6px;border:1px solid var(--hairline-strong);border-bottom-width:2px;border-radius:4px;background:var(--surface);color:var(--body);font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:10.5px;line-height:1.4}.oc-operation-card{width:min(720px,calc(100% - 42px));margin:var(--space-3) auto;padding:var(--space-4);border:1px solid var(--hairline-strong);border-left:4px solid var(--blue);border-radius:var(--radius-lg);background:color-mix(in srgb,var(--surface) 96%,var(--blue) 4%);box-shadow:var(--shadow-sm)}.oc-operation-card.is-succeeded{border-left-color:var(--green)}.oc-operation-card.is-failed,.oc-operation-card.is-timed-out{border-left-color:var(--red)}.oc-operation-card.is-needs-reconcile{border-left-color:var(--orange)}.oc-operation-head,.oc-operation-actions{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3)}.oc-operation-kicker{margin-bottom:2px;color:var(--muted);font-size:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}.oc-operation-state{padding:3px 9px;border-radius:999px;background:color-mix(in srgb,var(--blue) 12%,transparent);color:var(--blue);font-size:11px;font-weight:700}.oc-operation-state.is-succeeded{background:color-mix(in srgb,var(--green) 12%,transparent);color:var(--green)}.oc-operation-state.is-failed,.oc-operation-state.is-timed-out{background:color-mix(in srgb,var(--red) 12%,transparent);color:var(--red)}.oc-operation-state.is-needs-reconcile{background:color-mix(in srgb,var(--orange) 14%,transparent);color:var(--orange-active)}.oc-operation-id,.oc-operation-controller{margin-top:var(--space-2);color:var(--muted);font-size:12px;overflow-wrap:anywhere}.oc-operation-identities{display:flex;flex-wrap:wrap;gap:6px;margin-top:var(--space-3)}.oc-operation-identities span{padding:3px 7px;border:1px solid var(--hairline);border-radius:999px;background:var(--surface-raised);color:var(--muted);font-size:11px}.oc-operation-preview{max-height:150px;margin:var(--space-3) 0 0;padding:var(--space-2) var(--space-3);overflow:auto;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface-sunken);color:var(--body);font-size:11px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere}.oc-operation-preview.is-error{border-color:color-mix(in srgb,var(--red) 30%,var(--hairline));color:var(--red)}.oc-operation-actions{justify-content:flex-start;margin-top:var(--space-3)}.oc-operation-actions time{margin-left:auto;color:var(--muted-soft);font-size:11px}.oc-interview-start{margin:var(--space-3) auto}.oc-draft-card{margin:var(--space-3);padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface-raised)}.oc-draft-card>header,.oc-task-kind-card{display:flex;align-items:center;gap:var(--space-3);justify-content:space-between}.oc-interview-bubble{margin:var(--space-3) 0;padding:var(--space-3);border-left:3px solid var(--accent);background:var(--surface-sunken)}.oc-interview-bubble textarea{width:100%;min-height:72px;margin:var(--space-2) 0;resize:vertical}.oc-interview-bubble .btn+.btn{margin-left:var(--space-2)}.oc-draft-card pre{max-height:300px;overflow:auto;white-space:pre-wrap;font-size:11px}.oc-assessment{margin:var(--space-3) 0;color:var(--orange)}.oc-assessment.is-fresh,.oc-task-kind-card.is-confirmed{color:var(--green)}.oc-task-kind-card{margin-top:var(--space-3);padding:var(--space-3);border:1px solid var(--hairline);border-radius:var(--radius-md)}.oc-task-kind-card small{color:var(--muted)}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}.oc-human-gate-card{margin:12px 0;padding:14px;border:1px solid var(--border);border-left:4px solid var(--accent);border-radius:10px;background:var(--surface-raised)}.oc-human-gate-card header,.oc-human-gate-card footer{display:flex;justify-content:space-between;align-items:center;gap:12px}.oc-human-gate-card header div{display:grid;gap:3px}.oc-human-gate-card header span{font-size:12px;text-transform:uppercase}.oc-human-gate-challenges,.oc-human-gate-card ol{max-height:160px;overflow:auto}.oc-human-gate-card footer{margin-top:12px}.oc-human-gate-error{margin-top:8px;color:var(--danger)}.oc-human-gate-card.is-confirmed{border-left-color:var(--success)}.oc-task-context{margin:var(--space-3);padding:var(--space-3);border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface-raised)}.oc-task-context>header{display:flex;justify-content:space-between;gap:var(--space-2);align-items:center}.oc-task-context>header div{display:grid;gap:2px}.oc-task-context>header small,.oc-task-context>header span,.oc-task-context-empty{color:var(--text-muted)}.oc-task-context-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--space-2);margin-top:var(--space-3)}.oc-task-context-grid>div{display:grid;gap:4px;min-width:0;padding:var(--space-2);border:1px solid var(--hairline);border-radius:var(--radius-md)}.oc-task-context-grid span,.oc-task-context-grid small{overflow-wrap:anywhere}.oc-shortcut-palette{position:absolute;z-index:12;left:0;right:0;bottom:calc(100% + 8px);display:grid;padding:6px;border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface-raised);box-shadow:var(--shadow-lg)}.oc-shortcut-palette button{display:flex;gap:var(--space-3);align-items:center;border:0;border-radius:var(--radius-md);padding:9px 10px;color:inherit;background:transparent;text-align:left}.oc-shortcut-palette button:hover,.oc-shortcut-palette button.is-active{background:var(--surface-hover)}.oc-shortcut-palette button span{display:grid;gap:2px}.oc-shortcut-palette button small{color:var(--text-muted)}.oc-composer-inner{position:relative}@media(max-width:800px){.oc-task-context-grid{grid-template-columns:1fr}}.oc-file-palette{position:absolute;z-index:13;left:0;right:0;bottom:calc(100% + 8px);display:grid;max-height:240px;overflow:auto;padding:6px;border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface-raised);box-shadow:var(--shadow-lg)}.oc-file-palette button{border:0;padding:8px 10px;color:inherit;background:transparent;text-align:left;border-radius:var(--radius-md)}.oc-file-palette button:hover{background:var(--surface-hover)}.oc-runtime-controls{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px}.oc-runtime-controls select{max-width:220px;border:1px solid var(--hairline);border-radius:var(--radius-md);padding:5px 8px;color:inherit;background:var(--surface-raised)}.oc-runtime-controls select.oc-model-select{max-width:min(100%,360px);font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12px}.oc-runtime-controls .oc-process-summary{margin-left:0}.oc-usage{color:var(--text-muted);font-size:12px;font-variant-numeric:tabular-nums}.oc-runtime-context-menu{position:relative;margin-left:auto;flex-shrink:0}.oc-runtime-context-toggle.is-active{color:var(--orange-active);background:color-mix(in srgb,var(--orange) 10%,var(--surface))}.oc-runtime-context-panel{position:absolute;right:0;bottom:calc(100% + 8px);z-index:16;width:min(400px,calc(100vw - 32px));max-height:min(360px,45vh);display:flex;flex-direction:column;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-soft);animation:panel-in .16s ease;overflow:hidden}.oc-runtime-context-head{display:flex;align-items:center;gap:8px;flex-shrink:0;padding:10px 12px;border-bottom:1px solid var(--hairline);background:var(--surface-raised)}.oc-runtime-context-head strong{font-size:13px}.oc-runtime-context-meta{flex:1;font-size:11px;color:var(--muted)}.oc-runtime-context-close{flex-shrink:0;min-height:28px;padding:0 8px;font-size:12px}.oc-runtime-context-body{flex:1;min-height:0;overflow:auto;padding:10px 12px 12px}.oc-runtime-context-dl{margin:0;display:grid;gap:10px;font-size:12.5px}.oc-runtime-context-dl>div{display:grid;gap:3px}.oc-runtime-context-dl dt{margin:0;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}.oc-runtime-context-dl dd{margin:0;line-height:1.45;color:var(--body)}.oc-runtime-context-hash{display:inline-block;margin-left:6px;font-size:11px;color:var(--muted)}.oc-runtime-context-skills{margin:4px 0 0;padding:0;list-style:none;display:grid;gap:4px}.oc-runtime-context-skills li{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.oc-runtime-context-skills span{font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums}.oc-runtime-context-raw{margin-top:12px;padding-top:10px;border-top:1px solid var(--hairline);font-size:12px;color:var(--muted)}.oc-runtime-context-raw summary{cursor:pointer;-webkit-user-select:none;user-select:none}.oc-runtime-context-raw pre{margin:8px 0 0;max-height:140px;overflow:auto;padding:8px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--canvas-soft);font-size:11px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere}@media(max-width:800px){.oc-runtime-context-menu{margin-left:0;width:100%}.oc-runtime-context-panel{left:0;right:0;width:auto}}.oc-artifact-card{max-width:100%;margin:12px 0;padding:14px;overflow:hidden;border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface-raised)}.oc-artifact-card header{display:flex;justify-content:space-between;gap:12px}.oc-artifact-card pre{max-height:320px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere}.oc-artifact-card iframe{width:100%;min-height:280px;border:1px solid var(--hairline);background:#fff}.oc-load-history{display:block;margin:8px auto}.oc-attach-button{display:inline-flex;min-height:24px;align-items:center;color:var(--muted);cursor:pointer;font-size:12px}.oc-composer-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:10px 14px;margin-bottom:6px;font-size:11.5px;color:var(--muted)}.oc-composer-toolbar .oc-attach-button{margin-bottom:0}.oc-attachment-list{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px}.oc-attachment-list span{display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:4px 8px;border:1px solid var(--hairline);border-radius:999px;overflow-wrap:anywhere}.oc-attachment-list button{border:0;color:inherit;background:transparent;cursor:pointer}@media(max-width:640px){.oc-runtime-controls,.oc-composer-foot{align-items:stretch;flex-direction:column}.oc-runtime-controls select,.oc-runtime-controls button{width:100%;max-width:100%;min-height:44px}.oc-artifact-card{width:100%}.oc-composer{padding-inline:8px}}.oc-branch-actions{display:inline-flex;gap:.4rem;align-items:center;flex-wrap:wrap}.oc-branch-badge{padding:.15rem .45rem;border-radius:999px;border:1px solid currentColor;font-size:.72rem}.oc-branch-badge.is-branch{color:#f6ad55}.oc-branch-badge.is-mainline{color:#68d391}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Loop 操作台 · Operator Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CSRIhuzh.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Yyn3ynVv.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side PRD file import helpers for Console Happy Path.
|
|
3
|
+
* Validates extension/size, decodes strict UTF-8, composes multi-file bodies,
|
|
4
|
+
* and budgets operator JSON payloads.
|
|
5
|
+
*/
|
|
6
|
+
export const MAX_PRD_FILE_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
export const MAX_OPERATOR_JSON_BYTES = 10 * 1024 * 1024;
|
|
8
|
+
export const MAX_PRD_FILES = 8;
|
|
9
|
+
export const PRD_FILE_ACCEPT = ".md,.markdown,.txt,text/markdown,text/plain";
|
|
10
|
+
export const ALLOWED_PRD_EXTENSIONS = [".md", ".markdown", ".txt"];
|
|
11
|
+
const EXT_RE = /\.[^.]+$/;
|
|
12
|
+
export function prdFileExtension(fileName) {
|
|
13
|
+
const match = EXT_RE.exec(fileName.trim());
|
|
14
|
+
return match ? match[0].toLowerCase() : "";
|
|
15
|
+
}
|
|
16
|
+
export function isAllowedPrdExtension(fileName) {
|
|
17
|
+
const ext = prdFileExtension(fileName);
|
|
18
|
+
return ALLOWED_PRD_EXTENSIONS.includes(ext);
|
|
19
|
+
}
|
|
20
|
+
export function formatByteSize(bytes) {
|
|
21
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
22
|
+
return "0 B";
|
|
23
|
+
if (bytes < 1024)
|
|
24
|
+
return `${bytes} B`;
|
|
25
|
+
if (bytes < 1024 * 1024) {
|
|
26
|
+
const kib = bytes / 1024;
|
|
27
|
+
return `${kib >= 10 ? kib.toFixed(0) : kib.toFixed(1)} KiB`;
|
|
28
|
+
}
|
|
29
|
+
const mib = bytes / (1024 * 1024);
|
|
30
|
+
return `${mib >= 10 ? mib.toFixed(1) : mib.toFixed(2)} MiB`;
|
|
31
|
+
}
|
|
32
|
+
function fail(code, message) {
|
|
33
|
+
return { ok: false, code, message };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate an incoming FileList before decoding.
|
|
37
|
+
* `existingCount` supports append mode (existing + incoming ≤ MAX_PRD_FILES).
|
|
38
|
+
*/
|
|
39
|
+
export function validatePrdFileList(files, options) {
|
|
40
|
+
const list = files ? Array.from(files) : [];
|
|
41
|
+
const maxFiles = options?.maxFiles ?? MAX_PRD_FILES;
|
|
42
|
+
const existingCount = options?.existingCount ?? 0;
|
|
43
|
+
if (list.length === 0) {
|
|
44
|
+
return fail("file-count", "请选择至少一个 .md / .markdown / .txt 文件");
|
|
45
|
+
}
|
|
46
|
+
if (existingCount + list.length > maxFiles) {
|
|
47
|
+
return fail("file-count", `最多支持 ${maxFiles} 个文件(当前已有 ${existingCount} 个)`);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validate and decode a single PRD text file as strict UTF-8.
|
|
53
|
+
*/
|
|
54
|
+
export async function importPrdFile(file) {
|
|
55
|
+
const fileName = file.name?.trim() || "untitled.md";
|
|
56
|
+
if (!isAllowedPrdExtension(fileName)) {
|
|
57
|
+
return fail("unsupported-extension", "仅支持 .md、.markdown、.txt 文件");
|
|
58
|
+
}
|
|
59
|
+
if (file.size > MAX_PRD_FILE_BYTES) {
|
|
60
|
+
return fail("file-too-large", `文件不能超过 ${formatByteSize(MAX_PRD_FILE_BYTES)}`);
|
|
61
|
+
}
|
|
62
|
+
let buffer;
|
|
63
|
+
try {
|
|
64
|
+
buffer = await file.arrayBuffer();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return fail("read-failed", "文件读取失败,请重试或直接粘贴");
|
|
68
|
+
}
|
|
69
|
+
let text;
|
|
70
|
+
try {
|
|
71
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return fail("invalid-utf8", "文件不是有效 UTF-8,请另存为 UTF-8");
|
|
75
|
+
}
|
|
76
|
+
if (text.trim().length === 0) {
|
|
77
|
+
return fail("empty-content", "文件内容不能为空或只有空白");
|
|
78
|
+
}
|
|
79
|
+
const textBytes = new TextEncoder().encode(text).byteLength;
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
text,
|
|
83
|
+
fileName,
|
|
84
|
+
sourceBytes: file.size,
|
|
85
|
+
textBytes,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decode multiple files; any failure aborts the whole batch (no partial list).
|
|
90
|
+
*/
|
|
91
|
+
export async function importPrdFiles(files) {
|
|
92
|
+
if (files.length === 0) {
|
|
93
|
+
return fail("file-count", "请选择至少一个 .md / .markdown / .txt 文件");
|
|
94
|
+
}
|
|
95
|
+
if (files.length > MAX_PRD_FILES) {
|
|
96
|
+
return fail("file-count", `最多支持 ${MAX_PRD_FILES} 个文件`);
|
|
97
|
+
}
|
|
98
|
+
const documents = [];
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const result = await importPrdFile(file);
|
|
101
|
+
if (!result.ok)
|
|
102
|
+
return result;
|
|
103
|
+
documents.push({
|
|
104
|
+
fileName: result.fileName,
|
|
105
|
+
text: result.text,
|
|
106
|
+
sourceBytes: result.sourceBytes,
|
|
107
|
+
textBytes: result.textBytes,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, documents };
|
|
111
|
+
}
|
|
112
|
+
/** Browser-safe title for compose header (no Node deps). */
|
|
113
|
+
export function extractComposeTitle(text, fallbackFileName) {
|
|
114
|
+
const lines = text.split(/\r?\n/);
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
const heading = /^(#{1,6})\s+(.+)$/.exec(line.trim());
|
|
117
|
+
if (heading?.[2])
|
|
118
|
+
return heading[2].trim();
|
|
119
|
+
}
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
const trimmed = line.trim();
|
|
122
|
+
if (!trimmed)
|
|
123
|
+
continue;
|
|
124
|
+
if (/^---+$/.test(trimmed))
|
|
125
|
+
continue;
|
|
126
|
+
if (/^```/.test(trimmed))
|
|
127
|
+
continue;
|
|
128
|
+
if (/^<!--/.test(trimmed))
|
|
129
|
+
continue;
|
|
130
|
+
return trimmed.replace(/^>\s+/, "").slice(0, 120);
|
|
131
|
+
}
|
|
132
|
+
const base = fallbackFileName.replace(EXT_RE, "").trim();
|
|
133
|
+
return base || "需求";
|
|
134
|
+
}
|
|
135
|
+
export function slugifyDocumentName(value) {
|
|
136
|
+
const trimmed = value.trim().toLowerCase();
|
|
137
|
+
const slug = trimmed
|
|
138
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
139
|
+
.replace(/^-+|-+$/g, "")
|
|
140
|
+
.replace(/-{2,}/g, "-");
|
|
141
|
+
return slug || "requirement";
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Logical import-prd --name from a file name. Non-ASCII-only stems become part-N.
|
|
145
|
+
*/
|
|
146
|
+
export function documentNameFromFileName(fileName, index) {
|
|
147
|
+
const stem = fileName.replace(EXT_RE, "").trim();
|
|
148
|
+
const slug = slugifyDocumentName(stem);
|
|
149
|
+
if (slug === "requirement" && /[^\u0000-\u007f]/.test(stem)) {
|
|
150
|
+
return `part-${index + 1}`;
|
|
151
|
+
}
|
|
152
|
+
if (!slug || slug === "requirement") {
|
|
153
|
+
return stem ? `part-${index + 1}` : `part-${index + 1}`;
|
|
154
|
+
}
|
|
155
|
+
return slug;
|
|
156
|
+
}
|
|
157
|
+
export function ensureUniqueDocumentNames(docs) {
|
|
158
|
+
const used = new Set();
|
|
159
|
+
return docs.map((doc, index) => {
|
|
160
|
+
let name = documentNameFromFileName(doc.fileName, index);
|
|
161
|
+
if (used.has(name)) {
|
|
162
|
+
let n = 2;
|
|
163
|
+
while (used.has(`${name}-${n}`))
|
|
164
|
+
n += 1;
|
|
165
|
+
name = `${name}-${n}`;
|
|
166
|
+
}
|
|
167
|
+
used.add(name);
|
|
168
|
+
return {
|
|
169
|
+
name,
|
|
170
|
+
content: doc.text,
|
|
171
|
+
fileName: doc.fileName,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Compose editable PRD markdown from loaded documents.
|
|
177
|
+
* Single file: body equals original text (no source wrapper).
|
|
178
|
+
* Multiple: deterministic sections with primary-driven top title.
|
|
179
|
+
*/
|
|
180
|
+
export function composePrdDocuments(docs, options) {
|
|
181
|
+
if (docs.length === 0)
|
|
182
|
+
return "";
|
|
183
|
+
if (docs.length === 1) {
|
|
184
|
+
return docs[0].text;
|
|
185
|
+
}
|
|
186
|
+
const primaryIndex = Math.min(Math.max(0, options?.primaryIndex ?? 0), docs.length - 1);
|
|
187
|
+
const primary = docs[primaryIndex];
|
|
188
|
+
const title = extractComposeTitle(primary.text, primary.fileName);
|
|
189
|
+
const names = docs.map((d) => d.fileName).join(", ");
|
|
190
|
+
const parts = [
|
|
191
|
+
`<!-- composed-from: ${names} -->`,
|
|
192
|
+
"",
|
|
193
|
+
`# ${title}`,
|
|
194
|
+
"",
|
|
195
|
+
];
|
|
196
|
+
for (const doc of docs) {
|
|
197
|
+
parts.push(`## 来源:${doc.fileName}`, "", doc.text.trim(), "");
|
|
198
|
+
}
|
|
199
|
+
return `${parts.join("\n").trimEnd()}\n`;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Estimate whether an operator POST body fits under the HTTP JSON limit.
|
|
203
|
+
* Mirrors `postOperator` envelope shape.
|
|
204
|
+
*/
|
|
205
|
+
export function operatorRequestFits(action, actionParams, options) {
|
|
206
|
+
const envelope = {
|
|
207
|
+
action,
|
|
208
|
+
actionParams,
|
|
209
|
+
};
|
|
210
|
+
if (options?.clientRequestId) {
|
|
211
|
+
envelope.clientRequestId = options.clientRequestId;
|
|
212
|
+
}
|
|
213
|
+
const bytes = new TextEncoder().encode(JSON.stringify(envelope)).byteLength;
|
|
214
|
+
return bytes <= MAX_OPERATOR_JSON_BYTES;
|
|
215
|
+
}
|
|
216
|
+
export function operatorRequestTooLargeMessage() {
|
|
217
|
+
return "最终内容导致请求体超过服务端限制,请缩小文档或减少文件数量";
|
|
218
|
+
}
|
|
@@ -413,6 +413,18 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
413
413
|
if (!hasAssertableExpectedResult(expected)) {
|
|
414
414
|
findings.push(`${testCase.id} has no structured expected result`);
|
|
415
415
|
}
|
|
416
|
+
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
417
|
+
const mappedScripts = extractMappedPytestScripts(testCase.body);
|
|
418
|
+
if (mappedScripts.length === 0) {
|
|
419
|
+
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射 (expected ${expectedScript} from module ${path.basename(relativeFile)})`);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
for (const script of mappedScripts) {
|
|
423
|
+
if (script !== expectedScript) {
|
|
424
|
+
findings.push(`${testCase.id} automation mapping ${script} must equal module one-to-one path ${expectedScript} (from ${path.basename(relativeFile)})`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
416
428
|
}
|
|
417
429
|
}
|
|
418
430
|
const missing = input.requiredRequirementIds.filter((id) => !coveredAc.has(id));
|
|
@@ -454,6 +466,27 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
454
466
|
function collectMarkdownCaseIds(markdown) {
|
|
455
467
|
return splitCases(markdown).map((testCase) => testCase.id);
|
|
456
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Derive the stable module stem used for Markdown ↔ pytest one-to-one mapping.
|
|
471
|
+
* `testcase/md/<module>.md` → stem → `testcase/test_<stem>.py`.
|
|
472
|
+
* Example: `BE-HEALTH.md` / `be-health.md` / `order-api.md` → `be_health` / `order_api`.
|
|
473
|
+
*/
|
|
474
|
+
export function normalizeBackendTestModuleStem(moduleFileName) {
|
|
475
|
+
const base = path.basename(moduleFileName).replace(/\.md$/i, "");
|
|
476
|
+
const stem = base
|
|
477
|
+
.toLowerCase()
|
|
478
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
479
|
+
.replace(/^_+|_+$/g, "")
|
|
480
|
+
.replace(/_+/g, "_");
|
|
481
|
+
if (!stem) {
|
|
482
|
+
throw new Error(`cannot derive backend-test module stem from: ${moduleFileName}`);
|
|
483
|
+
}
|
|
484
|
+
return stem;
|
|
485
|
+
}
|
|
486
|
+
/** Expected pytest script path for a Markdown module file (basename or relative path). */
|
|
487
|
+
export function expectedBackendTestPytestScriptForMarkdownModule(moduleFileName) {
|
|
488
|
+
return `testcase/test_${normalizeBackendTestModuleStem(moduleFileName)}.py`;
|
|
489
|
+
}
|
|
457
490
|
function extractMappedPytestScripts(testCaseBody) {
|
|
458
491
|
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
459
492
|
if (!automation.trim())
|
|
@@ -462,6 +495,15 @@ function extractMappedPytestScripts(testCaseBody) {
|
|
|
462
495
|
.map((match) => match[1].replaceAll("\\", "/"))
|
|
463
496
|
.filter((value) => !value.includes("..")));
|
|
464
497
|
}
|
|
498
|
+
function isSafeBackendPytestScript(script, workspaceRoot) {
|
|
499
|
+
const absolute = path.resolve(workspaceRoot, script);
|
|
500
|
+
const relative = path.relative(workspaceRoot, absolute).replaceAll(path.sep, "/");
|
|
501
|
+
return (!relative.startsWith("..") &&
|
|
502
|
+
!path.isAbsolute(relative) &&
|
|
503
|
+
script.startsWith("testcase/") &&
|
|
504
|
+
!script.includes("..") &&
|
|
505
|
+
/^test_.*\.py$/i.test(path.basename(script)));
|
|
506
|
+
}
|
|
465
507
|
function testFunctionRegion(input) {
|
|
466
508
|
const functionLineStart = input.source.lastIndexOf("\n", input.functionIndex - 1) + 1;
|
|
467
509
|
const functionLine = input.source.slice(functionLineStart, input.functionHeaderEnd);
|
|
@@ -610,26 +652,50 @@ export async function collectBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
610
652
|
for (const file of files) {
|
|
611
653
|
if (path.basename(file).toLowerCase() === "readme.md")
|
|
612
654
|
continue;
|
|
655
|
+
const relativeFile = path.relative(workspaceRoot, file).replaceAll(path.sep, "/");
|
|
656
|
+
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
613
657
|
const markdown = await readFile(file, "utf8");
|
|
658
|
+
const mappedFromModule = new Set();
|
|
614
659
|
for (const testCase of splitCases(markdown)) {
|
|
615
|
-
for (const script of extractMappedPytestScripts(testCase.body))
|
|
660
|
+
for (const script of extractMappedPytestScripts(testCase.body)) {
|
|
661
|
+
mappedFromModule.add(script);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// Prefer explicit mappings, but recover to the deterministic module stem path
|
|
665
|
+
// when the model wrote the correct one-to-one file while Markdown still names
|
|
666
|
+
// a drifted script (common: health.md maps test_health.py but writer emitted
|
|
667
|
+
// test_be_health.py for BE-HEALTH.md, or the reverse).
|
|
668
|
+
if (mappedFromModule.size === 0) {
|
|
669
|
+
if (await exists(path.resolve(workspaceRoot, expectedScript))) {
|
|
670
|
+
scripts.add(expectedScript);
|
|
671
|
+
}
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
for (const script of mappedFromModule) {
|
|
675
|
+
if (!isSafeBackendPytestScript(script, workspaceRoot)) {
|
|
676
|
+
throw new Error(`unsafe mapped pytest script: ${script}`);
|
|
677
|
+
}
|
|
678
|
+
if (await exists(path.resolve(workspaceRoot, script))) {
|
|
616
679
|
scripts.add(script);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (isSafeBackendPytestScript(expectedScript, workspaceRoot) &&
|
|
683
|
+
(await exists(path.resolve(workspaceRoot, expectedScript)))) {
|
|
684
|
+
scripts.add(expectedScript);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
throw new Error(`mapped pytest script is missing: ${script} (module one-to-one path ${expectedScript} also missing)`);
|
|
617
688
|
}
|
|
618
689
|
}
|
|
619
690
|
if (scripts.size === 0) {
|
|
620
691
|
throw new Error("no pytest scripts are mapped by final Markdown cases");
|
|
621
692
|
}
|
|
622
693
|
const safeScripts = [];
|
|
623
|
-
for (const script of unique(scripts)) {
|
|
624
|
-
|
|
625
|
-
const relative = path.relative(workspaceRoot, absolute);
|
|
626
|
-
if (relative.startsWith("..") ||
|
|
627
|
-
path.isAbsolute(relative) ||
|
|
628
|
-
!script.startsWith("testcase/") ||
|
|
629
|
-
!/^test_.*\.py$/i.test(path.basename(script))) {
|
|
694
|
+
for (const script of unique([...scripts]).sort()) {
|
|
695
|
+
if (!isSafeBackendPytestScript(script, workspaceRoot)) {
|
|
630
696
|
throw new Error(`unsafe mapped pytest script: ${script}`);
|
|
631
697
|
}
|
|
632
|
-
if (!(await exists(
|
|
698
|
+
if (!(await exists(path.resolve(workspaceRoot, script)))) {
|
|
633
699
|
throw new Error(`mapped pytest script is missing: ${script}`);
|
|
634
700
|
}
|
|
635
701
|
safeScripts.push(script);
|
|
@@ -1076,10 +1142,10 @@ export function renderBackendTestFacts(input) {
|
|
|
1076
1142
|
"",
|
|
1077
1143
|
"</details>",
|
|
1078
1144
|
"",
|
|
1079
|
-
"##
|
|
1145
|
+
"## 覆盖率证据",
|
|
1080
1146
|
"",
|
|
1081
1147
|
"- Code coverage: unavailable unless a separate validated coverage artifact exists.",
|
|
1082
|
-
"-
|
|
1148
|
+
"- Coverage is reported only when a separate validated coverage artifact exists; this single execution does not produce a coverage result.",
|
|
1083
1149
|
].join("\n") + "\n");
|
|
1084
1150
|
}
|
|
1085
1151
|
export async function writeRunReport(runDir, fileName, content) {
|
|
@@ -3190,13 +3190,12 @@ function buildTestRetrospectNode(sources) {
|
|
|
3190
3190
|
"- AC coverage ratio / case counts MUST come from contracts/backend-test-case-manifest.json coverageSummary (or gate-derived fields). Do NOT invent coverage %.",
|
|
3191
3191
|
"- Automation coverage MUST use coverageSummary.generatedCount / coverageSummary.caseCount. If either field is missing, write unavailable; do not estimate.",
|
|
3192
3192
|
"- Code coverage MUST come only from the validated contracts/code-coverage-v1.json artifact generated by coverage.py/pytest-cov or JaCoCo. Show line, branch, function/method, covered, total, ratio, threshold, status, source scope, requirement IDs, tool, commit, and artifact hash.",
|
|
3193
|
-
"- Stability MUST come from independent Stability Evidence: use successfulRuns / recordedRuns, same suite/version, and require n≥5; a single run is unavailable.",
|
|
3194
3193
|
"- Use classify-backend-test-result-pi JSON as interpretive evidence only.",
|
|
3195
3194
|
"- NEVER rewrite a failed result as passed. Result v1 is authoritative for testOutcome; pipeline completion and L-5 readiness are separate conclusions.",
|
|
3196
3195
|
"",
|
|
3197
3196
|
"## Report Structure:",
|
|
3198
3197
|
"1. Maturity Rating with rationale",
|
|
3199
|
-
"2. Test Coverage Summary (Result v1 pass rate, AC coverage, automation coverage, code coverage
|
|
3198
|
+
"2. Test Coverage Summary (Result v1 pass rate, AC coverage, automation coverage, and code coverage)",
|
|
3200
3199
|
"3. Failed Test Analysis (failure/error details, category, confidence, evidence, and owner direction)",
|
|
3201
3200
|
"4. Defects (local Bug ledger in the same report directory; unavailable when absent)",
|
|
3202
3201
|
"5. Risks (Critical/High/Medium/Low, impact, controls, residual risk, treatment; Critical risks block L-5, High risks do not automatically block)",
|
|
@@ -3204,7 +3203,7 @@ function buildTestRetrospectNode(sources) {
|
|
|
3204
3203
|
"7. L-5 conclusion with blocking items",
|
|
3205
3204
|
"",
|
|
3206
3205
|
"## Rating Criteria:",
|
|
3207
|
-
"- L-5 ready requires pass rate=100%, AC coverage=100%, automation coverage≥90%,
|
|
3206
|
+
"- L-5 ready requires pass rate=100%, AC coverage=100%, automation coverage≥90%, line coverage≥80%, branch coverage≥70%, skipped=0, and no blocking Critical risk.",
|
|
3208
3207
|
"- Any required metric fail or unavailable means L-5 not-ready. Function/method coverage is displayed but not a gate. Preserve the existing A/B/C/D single-run rating separately.",
|
|
3209
3208
|
"",
|
|
3210
3209
|
"## Constraints:",
|
|
@@ -3257,13 +3256,13 @@ function buildL5MetricsNode(sources) {
|
|
|
3257
3256
|
outputContract: "Exactly one JSON object with status=ready|not-ready, metrics, and blockingItems; no file writes.",
|
|
3258
3257
|
subtask_prompt: [
|
|
3259
3258
|
"You are the independent L-5 metrics node at the end of the existing backend-test DAG.",
|
|
3260
|
-
"The direct upstream test-retrospect-pi output is the primary report to assess. Read it together with the run-owned Result v1, Case Manifest v1, Code Coverage v1
|
|
3259
|
+
"The direct upstream test-retrospect-pi output is the primary report to assess. Read it together with the run-owned Result v1, Case Manifest v1, and Code Coverage v1 artifacts when present.",
|
|
3261
3260
|
"Do not create a new DAG, rewrite the retrospective report, change test outcome, or modify any repository file.",
|
|
3262
3261
|
"Return exactly one JSON object and no surrounding prose.",
|
|
3263
|
-
'Required shape: {"status":"ready"|"not-ready","metrics":{"passRate":metric,"acCoverage":metric,"automationCoverage":metric,"
|
|
3262
|
+
'Required shape: {"status":"ready"|"not-ready","metrics":{"passRate":metric,"acCoverage":metric,"automationCoverage":metric,"lineCoverage":metric,"branchCoverage":metric,"skipped":metric,"criticalRisks":metric},"blockingItems":[string]}.',
|
|
3264
3263
|
"Each metric must contain numerator, denominator, ratio, threshold, status=pass|fail|unavailable, and reason (null only when passed).",
|
|
3265
3264
|
"Use only explicit evidence. Missing or invalid required evidence is unavailable, never zero or an estimate.",
|
|
3266
|
-
"L-5 ready requires pass rate=100%, AC coverage=100%, automation coverage>=90%,
|
|
3265
|
+
"L-5 ready requires pass rate=100%, AC coverage=100%, automation coverage>=90%, line coverage>=80%, branch coverage>=70%, skipped=0, and zero blocking Critical risks.",
|
|
3267
3266
|
"Function/method coverage is display-only and does not gate L-5. Preserve the distinction between L-5 maturity and the Result v1 outcome gate.",
|
|
3268
3267
|
].join("\n\n"),
|
|
3269
3268
|
};
|
|
@@ -3303,8 +3302,9 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3303
3302
|
"Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
|
|
3304
3303
|
"Create testcase/md/README.md as the concise entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in README.",
|
|
3305
3304
|
"Write each module as readable case cards. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. The only sections required by the deterministic validator are `### 前置条件`, `### 操作步骤`, and `### 预期结果` (legacy English aliases remain accepted). Add `测试目的`, `验收标准`, `需求依据`, `测试数据`, and `自动化映射` when useful for human readability; every automatable case should explicitly name its target pytest script under `自动化映射` so traceability can scan only that script.",
|
|
3305
|
+
"Name each module file with a stable lowercase business stem such as `testcase/md/health.md` or `testcase/md/resource_notes.md`. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
3306
3306
|
"Place steps and their expected results in a compact readable table when that improves clarity; otherwise keep numbered executable steps and numbered/bulleted independently assertable results. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
|
|
3307
|
-
"In `自动化映射`, record the planned script path and pytest function name when known. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
3307
|
+
"In `自动化映射`, record the planned script path and pytest function name when known, and keep the script path identical to the module one-to-one path above. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
3308
3308
|
intake.boundedSourceContext, "## Authoritative reference index", JSON.stringify(intake.referenceIndex, null, 2),
|
|
3309
3309
|
"For each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.",
|
|
3310
3310
|
"Read only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text.",
|
|
@@ -3317,8 +3317,8 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3317
3317
|
outputContract: "Review source fidelity and directly revise only testcase/md/**; return concise Markdown, never JSON.",
|
|
3318
3318
|
subtask_prompt: [
|
|
3319
3319
|
"Independently review generated Markdown cases against the task requirements and environment evidence. Treat the files as human-facing test documentation: require clear preconditions, executable steps and assertable expected results; improve names, purpose, metadata and automation mapping where useful while preserving exact machine IDs and technical literals.",
|
|
3320
|
-
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, vague results such as ‘符合预期’, and missing script/function mapping where it can be derived.",
|
|
3321
|
-
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, fix mappings/
|
|
3320
|
+
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, vague results such as ‘符合预期’, Case-ID-like module filenames (for example `BE-HEALTH.md`), and missing or drifted script/function mapping where it can be derived.",
|
|
3321
|
+
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, rename module files to stable lowercase stems when needed, fix automation mappings so each case points at `testcase/test_<module>.py` derived from that module filename, merge duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations exact. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
|
|
3322
3322
|
"Read only precise referenced source paths plus requirement sections needed for uncovered ACs. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
|
|
3323
3323
|
intake.boundedSourceContext, "## Authoritative reference index", JSON.stringify(intake.referenceIndex, null, 2),
|
|
3324
3324
|
"For each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.",
|
|
@@ -3334,7 +3334,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3334
3334
|
subtask_prompt: [
|
|
3335
3335
|
"Convert testcase/md/** to pytest using upstream environment and advisory validation evidence plus only bounded pytest config/conftest. A FAIL advisory report does not authorize inventing missing behavior; use the final Markdown facts that are present.",
|
|
3336
3336
|
"Ensure every final Markdown Case ID appears in at least one real pytest test function or pytest test class method region, preferably as `test_BE_<MODULE>_<NNN>_<description>` and in that function/method docstring. Module-level functions and class-based pytest methods are both supported. Multiple test functions may cover one Case ID; assertions come only from 预期结果/Expected Results and setup comes only from 前置条件 plus any optional 测试数据/自动化映射 or their legacy English aliases.",
|
|
3337
|
-
"Name each generated pytest file so it corresponds one-to-one with its source Markdown module file: for each `testcase/md/<module>.md` (excluding README.md), emit exactly one `testcase/test_<module>.py`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `testcase/md/resource_notes.md` maps to `testcase/test_resource_notes.py`, and `testcase/md/order-api.md` maps to `testcase/test_order_api.py`. Never merge multiple Markdown modules into one pytest file, never split one module across several files, and never invent pytest filenames unrelated to the Markdown modules.",
|
|
3337
|
+
"Name each generated pytest file so it corresponds one-to-one with its source Markdown module file: for each `testcase/md/<module>.md` (excluding README.md), emit exactly one `testcase/test_<module>.py`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `testcase/md/resource_notes.md` maps to `testcase/test_resource_notes.py`, `testcase/md/health.md` maps to `testcase/test_health.py`, `testcase/md/BE-HEALTH.md` maps to `testcase/test_be_health.py`, and `testcase/md/order-api.md` maps to `testcase/test_order_api.py`. If Markdown automation mapping names a different path than this module stem path, still write the module stem path and do not invent prefixes such as `test_be_*` unless the module filename itself normalizes to that stem. Never merge multiple Markdown modules into one pytest file, never split one module across several files, and never invent pytest filenames unrelated to the Markdown modules.",
|
|
3338
3338
|
"Generate a reusable HTTP logging helper (or equivalent client wrapper) and call it for every interface request. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions.",
|
|
3339
3339
|
"Compare timestamps and other semantically equivalent protocol values by parsed meaning, not byte-for-byte serialization. In particular, normalize valid ISO-8601 instants before equality/order assertions so differences such as omitted trailing fractional seconds do not create TestBug failures; preserve exact-string assertions only when the Markdown explicitly requires representation equality.",
|
|
3340
3340
|
"Before logging, recursively redact sensitive keys and header values including authorization, proxy-authorization, cookie, set-cookie, token, password, secret, api key and credentials. Never print full Authorization/Cookie values. Apply bounded truncation to serialized request and response bodies (with an explicit truncation marker) so large payloads cannot flood pytest or report artifacts.",
|
|
@@ -3346,7 +3346,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3346
3346
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
3347
3347
|
'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
|
|
3348
3348
|
].join("; ");
|
|
3349
|
-
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once.
|
|
3349
|
+
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Prefer the deterministic module one-to-one path when a mapped script is missing but the module stem file exists. Generate a native pytest-html self-contained report, then render the primary self-contained Chinese HTML report from the same pytest-html plus final Markdown case metadata without rerun. Keep 测试结论, quality status, failure overview, and a polished per-case result card with concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing a valid pytest-html report with per-case captured output, self-contained reports/backend-test.html and reports/backend-test.md, plus internal reports/backend-test-facts.md evidence; exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
|
|
3350
3350
|
if (execute.shell) {
|
|
3351
3351
|
execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
3352
3352
|
}
|
|
@@ -3359,11 +3359,11 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3359
3359
|
forbiddenPaths: forbidden,
|
|
3360
3360
|
outputContract: canWriteReport ? "Final Markdown report and L-5 conclusion under docs/test-reports/**; no JSON." : "Final Markdown report and L-5 conclusion in assistant output; no JSON or writes.",
|
|
3361
3361
|
subtask_prompt: [
|
|
3362
|
-
"Generate the final Markdown report from upstream facts and run-owned environment, advisory case-validation, advisory traceability,
|
|
3362
|
+
"Generate the final Markdown report from upstream facts and run-owned environment, advisory case-validation, advisory traceability, pytest-html and HTML evidence. Do not emit JSON.",
|
|
3363
3363
|
"Use this exact human-facing section order: 测试结论 → 执行概览 → 质量校验 → 失败分析 → 风险与建议 → 证据与 L-5. Put the decision and key numbers first, use compact tables/bullets, and keep headings concise. Do not paste entire upstream reports, duplicate per-case tables already present in facts, or repeat the same evidence in multiple sections; link to paths/hashes and quote only the findings needed for the conclusion.",
|
|
3364
3364
|
"Always state the exact PASS/FAIL status and findings from nodes 4 and 6. Their FAIL status does not block pytest, but it must remain visible as a quality/traceability risk and must never be rewritten as PASS.",
|
|
3365
|
-
"Include environment, case quality/review, automation mapping, exact pytest facts, failure classification/analysis, risks, regression recommendations, evidence paths/hashes, coverage
|
|
3366
|
-
"Never override Shell/
|
|
3365
|
+
"Include environment, case quality/review, automation mapping, exact pytest facts, failure classification/analysis, risks, regression recommendations, evidence paths/hashes, coverage availability, and L-5 READY/NOT READY.",
|
|
3366
|
+
"Never override Shell/pytest-html facts. L-5 requires pass=100%, AC=100%, automation>=90%, line>=80%, branch>=70%, skipped=0 and no blocking Critical risk.",
|
|
3367
3367
|
canWriteReport ? "Write only under docs/test-reports/**." : "Keep the full report in assistant output.",
|
|
3368
3368
|
].join("\n\n"),
|
|
3369
3369
|
};
|
|
@@ -3377,7 +3377,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3377
3377
|
...taskConfig.hardConstraints, ...STANDARD_GLOBAL_CONSTRAINTS,
|
|
3378
3378
|
"backend-test-dag uses exactly 8 real top-level tasks and executes pytest exactly once over only the safe scripts explicitly mapped by final Markdown cases.",
|
|
3379
3379
|
"Model nodes produce Markdown and pytest assets, never backend-test business JSON envelopes.",
|
|
3380
|
-
"Environment, advisory Markdown validation, advisory traceability,
|
|
3380
|
+
"Environment, advisory Markdown validation, advisory traceability, pytest-html, HTML and execution facts are deterministic evidence. Nodes 4 and 6 record findings without blocking nodes 5, 7 or 8.",
|
|
3381
3381
|
"Only Markdown case generation/review may read source facts; pytest generation must not read source/**.",
|
|
3382
3382
|
"Functional case IDs use BE-<MODULE>-<NNN>; production code/config, skip/xfail, repair and rerun are forbidden.",
|
|
3383
3383
|
],
|