@tea-agent/loop-agent 0.20.0 → 0.20.1
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/CHANGELOG.md +31 -0
- package/dist/application/dag/args.js +29 -0
- package/dist/application/dag/run-dag.js +3 -1
- package/dist/cli/command-definitions.js +15 -1
- package/dist/cli/program.js +11 -1
- package/dist/commands/dag-rerun-task.js +19 -0
- package/dist/commands/dag-rerun.js +111 -0
- package/dist/executors/shell-executor.js +1 -0
- package/dist/shared/operator/capabilities.js +54 -0
- package/dist/worker/console/index.js +1 -1
- package/dist/worker/console/inspect-split.js +82 -0
- package/dist/worker/console/operation-runner.js +3 -1
- package/dist/worker/console/operation-store.js +1 -0
- package/dist/worker/console/operator-actions.js +153 -2
- package/dist/worker/console/operator-user-error.js +10 -0
- package/dist/worker/console/pi-readiness.js +4 -0
- package/dist/worker/console/recovery-cta.js +116 -5
- package/dist/worker/console/recovery-selection.js +107 -0
- package/dist/worker/console/resolve-dag-run-for-task.js +50 -11
- package/dist/worker/console/routes.js +20 -0
- package/dist/worker/console/sibling-controller.js +12 -7
- package/dist/worker/console/static/assets/index-CUDke82y.js +18 -0
- package/dist/worker/console/static/assets/index-wSEksVSO.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observability/read-model.js +60 -0
- package/dist/worker/observe/spec-evidence.js +33 -0
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/views/dag-inspector.js +67 -4
- package/dist/worker/run-task/run-task.js +7 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +97 -2
- package/dist/workflows/dag/frontend-project-capability.js +6 -2
- package/dist/workflows/dag/frontend-test-result-contract.js +64 -0
- package/dist/workflows/dag/init-hybrid.js +91 -48
- package/dist/workflows/dag/node-execution.js +40 -6
- package/dist/workflows/dag/output-protocol.js +76 -0
- package/dist/workflows/dag/rerun-plan.js +611 -0
- package/dist/workflows/dag/rerun-run.js +497 -0
- package/dist/workflows/dag/rerun-task.js +284 -0
- package/dist/workflows/dag/retry-policy.js +20 -1
- package/dist/workflows/dag/runner.js +50 -0
- package/dist/workflows/dag/skill-snapshot.js +22 -3
- package/dist/workflows/dag/types.js +10 -0
- package/dist/workflows/dag/validate.js +11 -0
- package/dist/workflows/dag/workspace-checkpoint.js +163 -0
- package/docs/README.md +1 -0
- package/docs/templates/agent-dag.schema.json +17 -2
- package/docs/templates/frontend-test-case-checklist.md +16 -1
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +26 -2
- package/docs/templates/frontend-test-dag.json +65 -6
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +4 -1
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +5 -3
- package/skills/frontend-design-review/references/review-checklist.md +3 -2
- package/skills/frontend-implementation/references/design-spec.md +16 -8
- package/skills/frontend-review/SKILL.md +7 -1
- package/skills/frontend-review/references/review-findings.md +5 -1
- package/skills/frontend-verification/SKILL.md +5 -3
- package/skills/frontend-verification/references/verification-checklist.md +3 -2
- package/skills/loop-agent/references/command-reference.md +3 -0
- package/skills/playwright-cli-case-generator/SKILL.md +35 -7
- package/dist/worker/console/static/assets/index-3vsjZJHq.js +0 -16
- package/dist/worker/console/static/assets/index-i1wV4LrY.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%);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 - 1120px)/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}.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}.console-shell{width:min(100% - 48px,1120px);margin:0 auto;padding:var(--space-5) 0 var(--space-12, 48px)}.console-header{display:grid;gap:var(--space-4);grid-template-columns:1fr auto;align-items:center;margin-bottom:var(--space-4);padding-bottom:var(--space-4);border-bottom:1px solid var(--hairline)}.brand-block{display:grid;gap:2px;align-content:start;min-width:0}h1{margin:0;color:var(--ink);font-size:clamp(20px,2.2vw,24px);font-weight:650;line-height:1.2;letter-spacing:-.02em}.subtitle{max-width:58ch;margin:0;color:var(--body);font-size:13px;line-height:1.4}.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{position:relative;display:grid;gap:2px;min-width:140px;max-width:280px;border:1px solid var(--hairline);background:var(--surface);border-radius:var(--radius-md);padding:8px 32px 8px 12px;box-shadow:var(--shadow-panel)}.readiness:before{content:"";position:absolute;top:50%;right:12px;width:8px;height:8px;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{display:block;margin:0;color:var(--muted);font-size:11px;font-weight:600;line-height:1.2}.readiness strong{color:var(--ink);font-size:14px;font-weight:700;line-height:1.2}.readiness[data-state=ready] strong,.readiness[data-state=setup-required] strong,.readiness[data-state=degraded] strong{color:var(--ink)}.readiness .muted{margin:2px 0 0;font-size:12px;line-height:1.35}.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)}.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}.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,1120px);padding-top:var(--space-5)}.console-header{grid-template-columns:1fr}.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))}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
|
|
@@ -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-CUDke82y.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-wSEksVSO.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -1574,6 +1574,39 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1574
1574
|
})
|
|
1575
1575
|
: undefined;
|
|
1576
1576
|
const backendTest = await loadBackendTestProjection(runDir, nodes);
|
|
1577
|
+
const continuationRecord = state.continuation &&
|
|
1578
|
+
typeof state.continuation.parentRunId === "string" &&
|
|
1579
|
+
typeof state.continuation.effectiveFromNodeId === "string" &&
|
|
1580
|
+
typeof state.continuation.operatorSelectedNodeId === "string"
|
|
1581
|
+
? state.continuation
|
|
1582
|
+
: undefined;
|
|
1583
|
+
const rerunPlan = continuationRecord
|
|
1584
|
+
? await safeReadJson(path.join(runDir, ".runtime", "rerun-plan.json"))
|
|
1585
|
+
: undefined;
|
|
1586
|
+
const verification = rerunPlan && typeof rerunPlan.verificationPolicy === "object"
|
|
1587
|
+
? rerunPlan.verificationPolicy
|
|
1588
|
+
: undefined;
|
|
1589
|
+
const continuation = continuationRecord
|
|
1590
|
+
? {
|
|
1591
|
+
parentRunId: continuationRecord.parentRunId,
|
|
1592
|
+
effectiveFromNodeId: continuationRecord.effectiveFromNodeId,
|
|
1593
|
+
operatorSelectedNodeId: continuationRecord.operatorSelectedNodeId,
|
|
1594
|
+
...(verification &&
|
|
1595
|
+
(verification.mode === "inherited" || verification.mode === "rerun") &&
|
|
1596
|
+
Array.isArray(verification.nodeIds) &&
|
|
1597
|
+
verification.nodeIds.every((nodeId) => typeof nodeId === "string")
|
|
1598
|
+
? {
|
|
1599
|
+
verificationPolicy: {
|
|
1600
|
+
mode: verification.mode,
|
|
1601
|
+
nodeIds: verification.nodeIds,
|
|
1602
|
+
...(typeof verification.inheritedFromRunId === "string"
|
|
1603
|
+
? { inheritedFromRunId: verification.inheritedFromRunId }
|
|
1604
|
+
: {}),
|
|
1605
|
+
},
|
|
1606
|
+
}
|
|
1607
|
+
: {}),
|
|
1608
|
+
}
|
|
1609
|
+
: undefined;
|
|
1577
1610
|
return {
|
|
1578
1611
|
dagRunId,
|
|
1579
1612
|
status,
|
|
@@ -1590,6 +1623,7 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1590
1623
|
nodes,
|
|
1591
1624
|
edges,
|
|
1592
1625
|
dagPath: runDir,
|
|
1626
|
+
...(continuation ? { continuation } : {}),
|
|
1593
1627
|
...(backendTest ? { backendTest } : {}),
|
|
1594
1628
|
};
|
|
1595
1629
|
}
|
|
@@ -1699,6 +1733,10 @@ function parseDagNodes(parsed, rankByNode, modelByNode) {
|
|
|
1699
1733
|
const nodeStatus = readString(node, "status");
|
|
1700
1734
|
const outputPreview = nodeOutputPreview(node);
|
|
1701
1735
|
const errorPreview = nodeErrorPreview(node, nodeStatus);
|
|
1736
|
+
const attempts = Array.isArray(node.attempts) ? node.attempts : undefined;
|
|
1737
|
+
const origin = node.origin && typeof node.origin === "object" && !Array.isArray(node.origin)
|
|
1738
|
+
? node.origin
|
|
1739
|
+
: undefined;
|
|
1702
1740
|
nodes.push({
|
|
1703
1741
|
nodeId: resolvedId,
|
|
1704
1742
|
rank: rankByNode.get(nodeId) ?? rankByNode.get(resolvedId),
|
|
@@ -1711,6 +1749,15 @@ function parseDagNodes(parsed, rankByNode, modelByNode) {
|
|
|
1711
1749
|
finishedAt: readString(node, "finishedAt"),
|
|
1712
1750
|
...(outputPreview ? { outputPreview } : {}),
|
|
1713
1751
|
...(errorPreview ? { errorPreview } : {}),
|
|
1752
|
+
...(attempts && attempts.length > 0
|
|
1753
|
+
? { attemptCount: attempts.length }
|
|
1754
|
+
: {}),
|
|
1755
|
+
...(readString(node, "failureCategory")
|
|
1756
|
+
? { failureCategory: readString(node, "failureCategory") }
|
|
1757
|
+
: {}),
|
|
1758
|
+
...(origin?.kind === "imported" || origin?.kind === "executed"
|
|
1759
|
+
? { originKind: origin.kind }
|
|
1760
|
+
: {}),
|
|
1714
1761
|
});
|
|
1715
1762
|
}
|
|
1716
1763
|
return nodes.sort((a, b) => a.nodeId.localeCompare(b.nodeId));
|
|
@@ -1721,6 +1768,10 @@ function parseDagNodes(parsed, rankByNode, modelByNode) {
|
|
|
1721
1768
|
const nodeStatus = readString(node, "status");
|
|
1722
1769
|
const outputPreview = nodeOutputPreview(node);
|
|
1723
1770
|
const errorPreview = nodeErrorPreview(node, nodeStatus);
|
|
1771
|
+
const attempts = Array.isArray(node.attempts) ? node.attempts : undefined;
|
|
1772
|
+
const origin = node.origin && typeof node.origin === "object" && !Array.isArray(node.origin)
|
|
1773
|
+
? node.origin
|
|
1774
|
+
: undefined;
|
|
1724
1775
|
nodes.push({
|
|
1725
1776
|
nodeId,
|
|
1726
1777
|
rank: rankByNode.get(nodeId),
|
|
@@ -1733,6 +1784,15 @@ function parseDagNodes(parsed, rankByNode, modelByNode) {
|
|
|
1733
1784
|
finishedAt: readString(node, "finishedAt"),
|
|
1734
1785
|
...(outputPreview ? { outputPreview } : {}),
|
|
1735
1786
|
...(errorPreview ? { errorPreview } : {}),
|
|
1787
|
+
...(attempts && attempts.length > 0
|
|
1788
|
+
? { attemptCount: attempts.length }
|
|
1789
|
+
: {}),
|
|
1790
|
+
...(readString(node, "failureCategory")
|
|
1791
|
+
? { failureCategory: readString(node, "failureCategory") }
|
|
1792
|
+
: {}),
|
|
1793
|
+
...(origin?.kind === "imported" || origin?.kind === "executed"
|
|
1794
|
+
? { originKind: origin.kind }
|
|
1795
|
+
: {}),
|
|
1736
1796
|
});
|
|
1737
1797
|
}
|
|
1738
1798
|
return nodes.sort((a, b) => a.nodeId.localeCompare(b.nodeId));
|
|
@@ -47,6 +47,23 @@ function isSpecFilePath(filePath) {
|
|
|
47
47
|
function isKnowledgeBaseTool(toolName) {
|
|
48
48
|
return KB_CONNECTOR_TOOLS.has(toolName);
|
|
49
49
|
}
|
|
50
|
+
/** A repo-relative path references <repoRoot>/openspec/**. */
|
|
51
|
+
function isOpenspecPath(filePath) {
|
|
52
|
+
const normalized = filePath.replaceAll(path.sep, "/");
|
|
53
|
+
return normalized === "openspec" || normalized.startsWith("openspec/");
|
|
54
|
+
}
|
|
55
|
+
/** A search query targets the openspec/ directory. */
|
|
56
|
+
function isOpenspecSearch(query, searchPath) {
|
|
57
|
+
const lower = query.toLowerCase();
|
|
58
|
+
const normalizedPath = searchPath?.replaceAll(path.sep, "/").toLowerCase();
|
|
59
|
+
return (normalizedPath === "openspec" ||
|
|
60
|
+
normalizedPath?.startsWith("openspec/") === true ||
|
|
61
|
+
lower === "openspec" ||
|
|
62
|
+
lower === "openspec/" ||
|
|
63
|
+
lower.startsWith("openspec/") ||
|
|
64
|
+
lower.includes("openspec/**") ||
|
|
65
|
+
lower.includes("openspec/*"));
|
|
66
|
+
}
|
|
50
67
|
function resolveSessionEventsPath(repoRoot, dagRunId, nodeId) {
|
|
51
68
|
if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId)) {
|
|
52
69
|
return null;
|
|
@@ -278,6 +295,9 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
|
|
|
278
295
|
}
|
|
279
296
|
// Search/scan tool calls (grep, find, ls, glob)
|
|
280
297
|
if (["grep", "find", "ls", "glob"].includes(toolName)) {
|
|
298
|
+
const searchPath = typeof pairedInput.path === "string"
|
|
299
|
+
? pairedInput.path
|
|
300
|
+
: undefined;
|
|
281
301
|
const query = typeof pairedInput.pattern === "string"
|
|
282
302
|
? pairedInput.pattern
|
|
283
303
|
: typeof pairedInput.query === "string"
|
|
@@ -288,6 +308,7 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
|
|
|
288
308
|
searches.push({
|
|
289
309
|
tool: toolName,
|
|
290
310
|
query,
|
|
311
|
+
path: searchPath,
|
|
291
312
|
timestamp: ts,
|
|
292
313
|
});
|
|
293
314
|
if (toolCallId)
|
|
@@ -341,6 +362,16 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
|
|
|
341
362
|
if (status === "no-evidence") {
|
|
342
363
|
summaryLines.push("未观察到任何规范证据:无 skill 注入、无文件读取、无检索操作。");
|
|
343
364
|
}
|
|
365
|
+
// Separate openspec/** reads and searches for Dashboard display.
|
|
366
|
+
// Knowledge base and openspec are parallel sources.
|
|
367
|
+
const openspecReads = specReads.filter((r) => isOpenspecPath(r.path));
|
|
368
|
+
const openspecSearches = searches.filter((s) => isOpenspecSearch(s.query, s.path));
|
|
369
|
+
if (openspecReads.length > 0) {
|
|
370
|
+
summaryLines.push(`openspec 已读取 ${openspecReads.length} 个文件:${openspecReads.map((r) => r.path).join("、")}`);
|
|
371
|
+
}
|
|
372
|
+
if (openspecSearches.length > 0) {
|
|
373
|
+
summaryLines.push(`openspec 检索 ${openspecSearches.length} 次。`);
|
|
374
|
+
}
|
|
344
375
|
return {
|
|
345
376
|
dagRunId,
|
|
346
377
|
nodeId,
|
|
@@ -352,6 +383,8 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
|
|
|
352
383
|
specReads,
|
|
353
384
|
specSearches: searches,
|
|
354
385
|
knowledgeBaseQueries: kbQueries,
|
|
386
|
+
openspecReads,
|
|
387
|
+
openspecSearches,
|
|
355
388
|
summary: summaryLines.join(" "),
|
|
356
389
|
};
|
|
357
390
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<body data-shell="observe-console">
|
|
16
16
|
<a class="skip-link" href="#main-content">跳到主内容</a>
|
|
17
17
|
<nav class="embedded-operator-nav" aria-label="Operator 导航">
|
|
18
|
-
<a href="/"
|
|
18
|
+
<a href="/">返回操作台</a>
|
|
19
19
|
<span aria-current="page">观测</span>
|
|
20
20
|
</nav>
|
|
21
21
|
<div id="app">
|
|
@@ -233,10 +233,15 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
233
233
|
}
|
|
234
234
|
content.appendChild(statusSection);
|
|
235
235
|
|
|
236
|
-
const appendListSection = (title, entries, renderEntry) => {
|
|
237
|
-
if (!entries?.length) return;
|
|
236
|
+
const appendListSection = (title, entries, renderEntry, emptyMessage = "") => {
|
|
237
|
+
if (!entries?.length && !emptyMessage) return;
|
|
238
238
|
const section = el("div", "spec-evidence-section");
|
|
239
239
|
section.appendChild(el("h4", null, title));
|
|
240
|
+
if (!entries?.length) {
|
|
241
|
+
section.appendChild(el("p", "spec-evidence-summary", emptyMessage));
|
|
242
|
+
content.appendChild(section);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
240
245
|
const list = el("ul", "spec-evidence-list");
|
|
241
246
|
for (const entry of entries) list.appendChild(renderEntry(entry));
|
|
242
247
|
section.appendChild(list);
|
|
@@ -273,9 +278,15 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
273
278
|
return item;
|
|
274
279
|
});
|
|
275
280
|
appendListSection("显式需求编号", evidence.sourceBinding?.requirementIds, (id) => el("li", null, id));
|
|
281
|
+
const openspecReadPaths = new Set(
|
|
282
|
+
(evidence.openspecReads ?? []).map((entry) => entry.path),
|
|
283
|
+
);
|
|
284
|
+
const otherSpecReads = (evidence.specReads ?? []).filter(
|
|
285
|
+
(entry) => !openspecReadPaths.has(entry.path),
|
|
286
|
+
);
|
|
276
287
|
appendListSection(
|
|
277
|
-
|
|
278
|
-
|
|
288
|
+
`其他已读取规范文件(${otherSpecReads.length})`,
|
|
289
|
+
otherSpecReads,
|
|
279
290
|
(read) => {
|
|
280
291
|
const item = el("li", null);
|
|
281
292
|
const button = document.createElement("button");
|
|
@@ -327,6 +338,58 @@ async function renderSpecEvidence(content, dagRunId, nodeId, cachedEvidence) {
|
|
|
327
338
|
item.append(icon, document.createTextNode(` ${query.connector}`));
|
|
328
339
|
return item;
|
|
329
340
|
},
|
|
341
|
+
"unavailable:未观察到知识库查询;connector 可能未配置或未调用。",
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// openspec reads are displayed independently from skill/docs reads.
|
|
345
|
+
appendListSection(
|
|
346
|
+
`openspec 已读取(${evidence.openspecReads?.length ?? 0})`,
|
|
347
|
+
evidence.openspecReads,
|
|
348
|
+
(read) => {
|
|
349
|
+
const item = el("li", null);
|
|
350
|
+
const button = document.createElement("button");
|
|
351
|
+
button.type = "button";
|
|
352
|
+
button.className = "spec-evidence-file-btn";
|
|
353
|
+
button.setAttribute("data-source", "read");
|
|
354
|
+
button.setAttribute("data-path", read.path);
|
|
355
|
+
if (read.timestamp) {
|
|
356
|
+
button.setAttribute("data-read-at", read.timestamp);
|
|
357
|
+
}
|
|
358
|
+
button.id = `spec-evidence-btn-read-${read.path}`;
|
|
359
|
+
const icon = el("i", "ri-book-open-line");
|
|
360
|
+
icon.setAttribute("aria-hidden", "true");
|
|
361
|
+
button.append(icon, el("code", null, read.path));
|
|
362
|
+
if (read.timestamp) {
|
|
363
|
+
button.appendChild(
|
|
364
|
+
el(
|
|
365
|
+
"span",
|
|
366
|
+
"spec-evidence-time",
|
|
367
|
+
formatSessionEventTime({ timestamp: read.timestamp }),
|
|
368
|
+
),
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
button.addEventListener("click", () =>
|
|
372
|
+
onSpecEvidenceFileClick(content, dagRunId, nodeId, "read", read.path, button),
|
|
373
|
+
);
|
|
374
|
+
item.appendChild(button);
|
|
375
|
+
return item;
|
|
376
|
+
},
|
|
377
|
+
evidence.openspecSearches?.length
|
|
378
|
+
? "已检索但未观察到 openspec 成功读取;若生成期存在候选,预写门禁会阻断 writer。"
|
|
379
|
+
: "unavailable:未观察到 openspec 成功读取。",
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
appendListSection(
|
|
383
|
+
`openspec 检索(${evidence.openspecSearches?.length ?? 0})`,
|
|
384
|
+
evidence.openspecSearches,
|
|
385
|
+
(search) => {
|
|
386
|
+
const item = el("li", null);
|
|
387
|
+
const icon = el("i", "ri-search-line");
|
|
388
|
+
icon.setAttribute("aria-hidden", "true");
|
|
389
|
+
item.append(icon, document.createTextNode(` ${search.tool}: ${search.query}`));
|
|
390
|
+
return item;
|
|
391
|
+
},
|
|
392
|
+
"unavailable:未观察到 openspec 检索。",
|
|
330
393
|
);
|
|
331
394
|
|
|
332
395
|
if (evidence.status === "source-bound" || evidence.status === "spec-injected" || evidence.status === "no-evidence") {
|
|
@@ -117,6 +117,13 @@ export async function runTaskSpec(options) {
|
|
|
117
117
|
options.repoRoot,
|
|
118
118
|
"--run-id",
|
|
119
119
|
workerRunId,
|
|
120
|
+
"--worker-association",
|
|
121
|
+
JSON.stringify({
|
|
122
|
+
schemaVersion: 1,
|
|
123
|
+
workerRunId,
|
|
124
|
+
featureId: options.taskSpec.feature_id,
|
|
125
|
+
taskId: options.taskSpec.id,
|
|
126
|
+
}),
|
|
120
127
|
"--events-jsonl",
|
|
121
128
|
dagEventsPath,
|
|
122
129
|
...maxConcurrentArgs(options.taskSpec),
|
|
@@ -30,6 +30,80 @@ function firstVerdictLine(text) {
|
|
|
30
30
|
}
|
|
31
31
|
return "";
|
|
32
32
|
}
|
|
33
|
+
function eventArgs(event) {
|
|
34
|
+
return event.args ?? event.toolInput ?? event.input ?? {};
|
|
35
|
+
}
|
|
36
|
+
function toRepoRelativePath(filePath, repoRoot) {
|
|
37
|
+
const resolved = path.resolve(repoRoot, filePath);
|
|
38
|
+
const relative = path.relative(repoRoot, resolved);
|
|
39
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return relative.replaceAll(path.sep, "/");
|
|
43
|
+
}
|
|
44
|
+
function isFailedToolResult(event) {
|
|
45
|
+
return (event.isError === true ||
|
|
46
|
+
event.toolResult?.error != null ||
|
|
47
|
+
event.toolResult?.ok === false ||
|
|
48
|
+
event.result?.error != null ||
|
|
49
|
+
event.result?.ok === false);
|
|
50
|
+
}
|
|
51
|
+
async function checkOpenspecReadEvidence(input) {
|
|
52
|
+
const { runDir, candidatePaths, planNodeId, reviewNodeId, repoRoot } = input;
|
|
53
|
+
if (candidatePaths.length === 0)
|
|
54
|
+
return [];
|
|
55
|
+
const matched = new Set();
|
|
56
|
+
const normalizedCandidates = new Set(candidatePaths
|
|
57
|
+
.map((candidate) => toRepoRelativePath(candidate, repoRoot))
|
|
58
|
+
.filter((candidate) => Boolean(candidate?.startsWith("openspec/"))));
|
|
59
|
+
for (const nodeId of [planNodeId, reviewNodeId]) {
|
|
60
|
+
const eventsPath = path.join(runDir, nodeId, "session-events.jsonl");
|
|
61
|
+
try {
|
|
62
|
+
const raw = await readFile(eventsPath, "utf8");
|
|
63
|
+
const startedReads = new Map();
|
|
64
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
if (!trimmed)
|
|
67
|
+
continue;
|
|
68
|
+
let event;
|
|
69
|
+
try {
|
|
70
|
+
event = JSON.parse(trimmed);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (event.type === "tool_execution_start" &&
|
|
76
|
+
event.toolName === "read" &&
|
|
77
|
+
event.toolCallId) {
|
|
78
|
+
const readPath = eventArgs(event).path;
|
|
79
|
+
if (typeof readPath === "string") {
|
|
80
|
+
startedReads.set(event.toolCallId, readPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else if (event.type === "tool_execution_end" &&
|
|
84
|
+
event.toolName === "read" &&
|
|
85
|
+
event.toolCallId !== undefined) {
|
|
86
|
+
const readPath = startedReads.get(event.toolCallId);
|
|
87
|
+
const relativePath = readPath
|
|
88
|
+
? toRepoRelativePath(readPath, repoRoot)
|
|
89
|
+
: null;
|
|
90
|
+
if (relativePath &&
|
|
91
|
+
!isFailedToolResult(event) &&
|
|
92
|
+
normalizedCandidates.has(relativePath)) {
|
|
93
|
+
matched.add(relativePath);
|
|
94
|
+
}
|
|
95
|
+
startedReads.delete(event.toolCallId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error.code === "ENOENT")
|
|
101
|
+
continue;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return [...matched];
|
|
106
|
+
}
|
|
33
107
|
export async function runFrontendPrewriteGate(input) {
|
|
34
108
|
const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
|
|
35
109
|
const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
|
|
@@ -55,6 +129,18 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
55
129
|
if (!input.config.allowedMockStrategies.includes(contract.mockApi.strategy)) {
|
|
56
130
|
throw new Error(`frontend prewrite gate blocked mock strategy ${contract.mockApi.strategy}; allowed=${input.config.allowedMockStrategies.join(",")}`);
|
|
57
131
|
}
|
|
132
|
+
const candidatePaths = input.config.openspecCandidatePaths ?? [];
|
|
133
|
+
const openspecReadPaths = await checkOpenspecReadEvidence({
|
|
134
|
+
runDir: input.runDir,
|
|
135
|
+
candidatePaths,
|
|
136
|
+
planNodeId,
|
|
137
|
+
reviewNodeId,
|
|
138
|
+
repoRoot: input.repoRoot ?? process.cwd(),
|
|
139
|
+
});
|
|
140
|
+
if (candidatePaths.length > 0 && openspecReadPaths.length === 0) {
|
|
141
|
+
const checkedNodes = [planNodeId, reviewNodeId].join(", ");
|
|
142
|
+
throw new Error(`openspec gate blocked: ${candidatePaths.length} candidate(s) [${candidatePaths.join(", ")}] not read by ${checkedNodes}; writer not authorized.`);
|
|
143
|
+
}
|
|
58
144
|
return {
|
|
59
145
|
ok: true,
|
|
60
146
|
planNodeId,
|
|
@@ -62,10 +148,12 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
62
148
|
verdict,
|
|
63
149
|
mockStrategy: contract.mockApi.strategy,
|
|
64
150
|
artifact,
|
|
151
|
+
openspecReadPaths,
|
|
152
|
+
openspecCandidatePaths: candidatePaths,
|
|
65
153
|
};
|
|
66
154
|
}
|
|
67
155
|
export function formatFrontendPrewriteGateStdout(result) {
|
|
68
|
-
|
|
156
|
+
const lines = [
|
|
69
157
|
"Frontend prewrite gate: pass",
|
|
70
158
|
`Plan: ${result.planNodeId}`,
|
|
71
159
|
`Review: ${result.reviewNodeId}`,
|
|
@@ -73,5 +161,12 @@ export function formatFrontendPrewriteGateStdout(result) {
|
|
|
73
161
|
`Structured artifact: ${result.artifact.path}`,
|
|
74
162
|
`Schema: ${result.artifact.schemaId}`,
|
|
75
163
|
`SHA-256: ${result.artifact.sha256}`,
|
|
76
|
-
]
|
|
164
|
+
];
|
|
165
|
+
if (result.openspecReadPaths.length > 0) {
|
|
166
|
+
lines.push(`openspec read: ${result.openspecReadPaths.join(", ")}`);
|
|
167
|
+
}
|
|
168
|
+
else if (result.openspecCandidatePaths.length === 0) {
|
|
169
|
+
lines.push("openspec: unavailable");
|
|
170
|
+
}
|
|
171
|
+
return lines.join("\n");
|
|
77
172
|
}
|
|
@@ -105,6 +105,7 @@ export async function discoverFrontendProjectCapability(repoRoot) {
|
|
|
105
105
|
const pkgPath = path.join(repoRoot, "package.json");
|
|
106
106
|
const pkgRaw = await readJson(pkgPath);
|
|
107
107
|
if (!pkgRaw || typeof pkgRaw !== "object") {
|
|
108
|
+
const openspec = await listOpenspec(repoRoot);
|
|
108
109
|
const base = {
|
|
109
110
|
schemaVersion: 1,
|
|
110
111
|
framework: "unknown",
|
|
@@ -115,14 +116,17 @@ export async function discoverFrontendProjectCapability(repoRoot) {
|
|
|
115
116
|
testRunner: [],
|
|
116
117
|
mock: [],
|
|
117
118
|
a11y: { status: "unknown", tools: [], evidencePaths: [] },
|
|
118
|
-
evidencePaths:
|
|
119
|
+
evidencePaths: openspec.slice(0, 5),
|
|
119
120
|
reasons: ["package.json missing or unreadable"],
|
|
120
121
|
designEvidence: {
|
|
121
|
-
normativePaths:
|
|
122
|
+
normativePaths: openspec,
|
|
122
123
|
advisoryPaths: [],
|
|
123
124
|
conflicts: [],
|
|
124
125
|
},
|
|
125
126
|
};
|
|
127
|
+
if (openspec.length) {
|
|
128
|
+
base.reasons.push(`openspec/ files discovered: ${openspec.slice(0, 5).join(", ")}`);
|
|
129
|
+
}
|
|
126
130
|
return { ...base, adapterGuidance: buildAdapterGuidance(base) };
|
|
127
131
|
}
|
|
128
132
|
evidencePaths.push("package.json");
|
|
@@ -185,3 +185,67 @@ export function buildFrontendTestOutcomeGateShellSnippet(options) {
|
|
|
185
185
|
`node -e 'const fs=require("fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));const ok=r.outcome==="passed"&&r.integrationMode==="real"&&Number(r.totals?.failed||0)===0&&Number(r.totals?.blocked||0)===0&&Array.isArray(r.acceptanceCoverage?.missing)&&r.acceptanceCoverage.missing.length===0;console.log("frontend-test outcome="+r.outcome+" integrationMode="+r.integrationMode);if(!ok)process.exit(1);' "\${RESULT}"`,
|
|
186
186
|
].join("; ");
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Shared frontend-test evidence gate for map children + node 7.
|
|
190
|
+
* - Hard fail only on manifest/path integrity (unsafe evidenceDir / escape).
|
|
191
|
+
* - Missing or malformed case-result/execution is healed to status=blocked
|
|
192
|
+
* with blockedReason=invalid-evidence-shape so result materialize + retrospect can run.
|
|
193
|
+
*/
|
|
194
|
+
export function buildFrontendCaseEvidenceValidateShellSnippet() {
|
|
195
|
+
const body = [
|
|
196
|
+
"const fs=require('fs'),path=require('path');",
|
|
197
|
+
"const manifestPath='testcase/frontend/cases/manifest.json';",
|
|
198
|
+
"if(!fs.existsSync(manifestPath))throw new Error('missing '+manifestPath);",
|
|
199
|
+
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
|
|
200
|
+
"if(!Array.isArray(manifest.cases))throw new Error('invalid frontend case manifest');",
|
|
201
|
+
"const statuses=new Set(['passed','failed','blocked']);",
|
|
202
|
+
"const issues=[];",
|
|
203
|
+
"let hardFail=false;",
|
|
204
|
+
"let evidenceContentCount=0;",
|
|
205
|
+
"function hasEvidenceContent(dir){if(!fs.existsSync(dir))return false;const pending=[dir];while(pending.length){const current=pending.pop();for(const entry of fs.readdirSync(current,{withFileTypes:true})){const candidate=path.join(current,entry.name);if(entry.isDirectory()){pending.push(candidate);}else if(entry.isFile()&&fs.statSync(candidate).size>0){return true;}}}return false;}",
|
|
206
|
+
"function writeBlocked(dir,caseId,reason,detail){",
|
|
207
|
+
" fs.mkdirSync(dir,{recursive:true});",
|
|
208
|
+
" const execution=path.join(dir,'execution.md');",
|
|
209
|
+
" const note='# '+caseId+'\\n\\nStatus: blocked\\n\\nReason: '+reason+(detail?('\\n\\nDetail: '+detail):'')+'\\n';",
|
|
210
|
+
" fs.writeFileSync(execution,note);",
|
|
211
|
+
" fs.writeFileSync(path.join(dir,'case-result.json'),JSON.stringify({caseId:caseId,status:'blocked',blockedReason:reason,evidencePaths:['execution.md']},null,2)+'\\n');",
|
|
212
|
+
"}",
|
|
213
|
+
"function isSafeRel(p){return typeof p==='string'&&p.length>0&&!path.isAbsolute(p)&&!p.includes('..');}",
|
|
214
|
+
"for(const c of manifest.cases){",
|
|
215
|
+
" const id=c&&typeof c.caseId==='string'?c.caseId:'?';",
|
|
216
|
+
" const dir=c&&c.evidenceDir;",
|
|
217
|
+
" const prefix='testcase/frontend/evidence/'+id;",
|
|
218
|
+
" if(!c||typeof c.caseId!=='string'||typeof dir!=='string'||!isSafeRel(dir)||!(dir===prefix||dir.startsWith(prefix+'/'))){",
|
|
219
|
+
" hardFail=true; issues.push({ruleId:'unsafe-evidence-dir',caseId:id,detail:String(dir)}); continue;",
|
|
220
|
+
" }",
|
|
221
|
+
" const execution=path.join(dir,'execution.md');",
|
|
222
|
+
" const resultPath=path.join(dir,'case-result.json');",
|
|
223
|
+
" if(hasEvidenceContent(dir))evidenceContentCount++;",
|
|
224
|
+
" let healReason=null; let healDetail=null;",
|
|
225
|
+
" if(!fs.existsSync(execution)){healReason='invalid-evidence-shape';healDetail='missing execution.md';}",
|
|
226
|
+
" if(!fs.existsSync(resultPath)){healReason='invalid-evidence-shape';healDetail=(healDetail?healDetail+'; ':'')+'missing case-result.json';}",
|
|
227
|
+
" let result=null;",
|
|
228
|
+
" if(!healReason){",
|
|
229
|
+
" try{result=JSON.parse(fs.readFileSync(resultPath,'utf8'));}catch(e){healReason='invalid-evidence-shape';healDetail='invalid JSON case-result.json';}",
|
|
230
|
+
" }",
|
|
231
|
+
" if(!healReason){",
|
|
232
|
+
" if(!result||result.caseId!==id||!statuses.has(result.status)||!Array.isArray(result.evidencePaths)||result.evidencePaths.some(p=>!isSafeRel(p))){",
|
|
233
|
+
" healReason='invalid-evidence-shape';healDetail='caseId/status/evidencePaths contract';",
|
|
234
|
+
" } else if(result.status==='passed'&&(result.evidencePaths.length<1||!result.evidencePaths.some(p=>/\\.(png|jpg|jpeg|webp|zip|har|webm|mp4|md)$/i.test(p)))){",
|
|
235
|
+
" healReason='invalid-evidence-shape';healDetail='passed requires browser evidence path';",
|
|
236
|
+
" } else if(result.status==='blocked'&&(typeof result.blockedReason!=='string'||!result.blockedReason.trim())){",
|
|
237
|
+
" healReason='invalid-evidence-shape';healDetail='blocked requires blockedReason';",
|
|
238
|
+
" }",
|
|
239
|
+
" }",
|
|
240
|
+
" if(healReason){",
|
|
241
|
+
" writeBlocked(dir,id,healReason,healDetail);",
|
|
242
|
+
" issues.push({ruleId:healReason,caseId:id,detail:healDetail,healed:true});",
|
|
243
|
+
" }",
|
|
244
|
+
"}",
|
|
245
|
+
"if(hardFail){console.error('frontend-test evidence hard-fail: '+JSON.stringify(issues)); process.exit(1);}",
|
|
246
|
+
"if(evidenceContentCount===0){console.error('frontend-test evidence hard-fail: no frontend case evidence content'); process.exit(1);}",
|
|
247
|
+
"const healed=issues.filter(i=>i.healed).length;",
|
|
248
|
+
"console.log('frontend case evidence validation ok cases='+manifest.cases.length+' healed='+healed+(issues.length?(' issues='+JSON.stringify(issues)):''));",
|
|
249
|
+
].join("");
|
|
250
|
+
return ["node -e", JSON.stringify(body)].join(" ");
|
|
251
|
+
}
|