@tea-agent/loop-agent 0.20.1-beta.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 +24 -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/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/static/index.html +1 -1
- package/dist/worker/run-task/run-task.js +7 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +64 -0
- package/dist/workflows/dag/init-hybrid.js +90 -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 +7 -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/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));
|
|
@@ -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">
|
|
@@ -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),
|
|
@@ -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
|
+
}
|
|
@@ -7,7 +7,8 @@ import { DAG_AGENT_RUNTIME_PI_ONLY, DAG_REPAIR_WRITER_PROTOCOL_EXPLICIT_NODE_V1,
|
|
|
7
7
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
8
8
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
9
9
|
import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
|
|
10
|
-
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
10
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, } from "./retry-policy.js";
|
|
11
|
+
import { REVIEW_VERDICT_OUTPUT_PROTOCOL } from "./output-protocol.js";
|
|
11
12
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
12
13
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
13
14
|
import { buildAuthoritySurfaceAuditNode, buildAuthoritySurfaceGateNode, resolveAuthoritySurfaceAudit, } from "./authority-surface.js";
|
|
@@ -22,7 +23,7 @@ import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "
|
|
|
22
23
|
import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
|
|
23
24
|
import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
|
|
24
25
|
import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js";
|
|
25
|
-
import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
|
|
26
|
+
import { buildFrontendCaseEvidenceValidateShellSnippet, buildFrontendTestOutcomeGateShellSnippet, } from "./frontend-test-result-contract.js";
|
|
26
27
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
27
28
|
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
28
29
|
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
|
|
@@ -3139,12 +3140,15 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3139
3140
|
function buildFrontendTestHybridDag(sources) {
|
|
3140
3141
|
const rawFrontendTest = sources.taskConfig.frontendTest;
|
|
3141
3142
|
const config = {
|
|
3142
|
-
|
|
3143
|
+
// Default 32: common FE suites cover ~24 AC with multi-dimension cases; 20 caused map maxExpandedNodes failures.
|
|
3144
|
+
maxCasesPerBatch: rawFrontendTest?.maxCasesPerBatch ?? 32,
|
|
3143
3145
|
maxTokensPerCase: rawFrontendTest?.maxTokensPerCase,
|
|
3144
3146
|
maxTotalTokens: rawFrontendTest?.maxTotalTokens,
|
|
3145
3147
|
reviewMode: rawFrontendTest?.reviewMode ?? "off",
|
|
3146
3148
|
strictOutcomeGate: rawFrontendTest?.strictOutcomeGate === true,
|
|
3147
3149
|
};
|
|
3150
|
+
const declaredRequirementIds = buildDagSourceBinding(sources).requirementIds;
|
|
3151
|
+
const declaredAcIds = declaredRequirementIds.filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
|
|
3148
3152
|
const reviewMode = config.reviewMode;
|
|
3149
3153
|
const blockingReview = reviewMode === "blocking";
|
|
3150
3154
|
const strictOutcomeGate = config.strictOutcomeGate;
|
|
@@ -3158,6 +3162,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3158
3162
|
const ragWriteSet = ["testcase/frontend/rag/**"];
|
|
3159
3163
|
const casesWriteSet = ["testcase/frontend/cases/**"];
|
|
3160
3164
|
const evidenceRoot = "testcase/frontend/evidence";
|
|
3165
|
+
const declaredAcIdsLiteral = JSON.stringify(declaredAcIds);
|
|
3166
|
+
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
3161
3167
|
const checklistValidation = [
|
|
3162
3168
|
"node -e",
|
|
3163
3169
|
JSON.stringify([
|
|
@@ -3169,20 +3175,36 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3169
3175
|
"if(!manifestPath)throw new Error('checklist: missing manifest.draft.json or manifest.json');",
|
|
3170
3176
|
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
|
|
3171
3177
|
"if(!Array.isArray(manifest.cases)||manifest.cases.length===0)throw new Error('checklist: empty cases');",
|
|
3178
|
+
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
3172
3179
|
"const issues=[];",
|
|
3173
3180
|
"const openRe=/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+https?:\\/\\/\\S+/i;",
|
|
3174
3181
|
"const prodRe=/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i;",
|
|
3175
3182
|
"const codeRe=/\\b(pytest|playwright\\.test|@playwright\\/test)\\b/i;",
|
|
3183
|
+
"const barePwRe=/(?:^|[\\s\"'(])(?:npx\\s+playwright\\b|playwright\\s+test\\b|from\\s+['\"]@playwright\\/|require\\(['\"]@playwright\\/|import\\s+.*@playwright\\/|(?<![\\w-])playwright(?!-cli)\\b)/i;",
|
|
3184
|
+
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
|
|
3185
|
+
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
3176
3186
|
"for(const c of manifest.cases){",
|
|
3177
3187
|
" const id=c&&c.caseId||'?';",
|
|
3188
|
+
" if(typeof c.caseId!=='string'||!caseIdRe.test(c.caseId))issues.push({ruleId:'case-id-shape',caseId:id,detail:'caseId must be FE-<FEATURE>-<NNN>-<dimension>, never AC-FE-*'});",
|
|
3189
|
+
" if(typeof c.caseId==='string'&&/^AC-/i.test(c.caseId))issues.push({ruleId:'case-id-is-ac',caseId:id,detail:'do not use acceptance id as caseId; put AC-FE-* only in acIds'});",
|
|
3178
3190
|
" const casePath=typeof c.casePath==='string'?c.casePath:null;",
|
|
3179
3191
|
" if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
|
|
3192
|
+
" if(typeof c.caseId==='string'&&casePath!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
|
|
3180
3193
|
" const body=fs.readFileSync(casePath,'utf8');",
|
|
3181
3194
|
" if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome --headed <absolute-url>'});",
|
|
3182
3195
|
" const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+--headed\\s+(https?:\\/\\/\\S+)/i);",
|
|
3183
3196
|
" if(m){const url=m[1].replace(/[)\\]},.\"']+$/,''); if(prodRe.test(url))issues.push({ruleId:'production-url',caseId:id,detail:url});}",
|
|
3184
3197
|
" if(codeRe.test(body))issues.push({ruleId:'no-test-source',caseId:id,detail:'pytest/playwright test source forbidden'});",
|
|
3185
|
-
" if(
|
|
3198
|
+
" if(barePwRe.test(body))issues.push({ruleId:'playwright-cli-only',caseId:id,detail:'only playwright-cli skill commands allowed; bare Playwright CLI/API/test runner forbidden'});",
|
|
3199
|
+
" if(!Array.isArray(c.acIds)||c.acIds.length===0)issues.push({ruleId:'ac-mapping',caseId:id,detail:'acIds required (AC-FE-* acceptance ids, not caseId)'});",
|
|
3200
|
+
" else {",
|
|
3201
|
+
" for(const ac of c.acIds){",
|
|
3202
|
+
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
3203
|
+
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
3204
|
+
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
3205
|
+
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
3206
|
+
" }",
|
|
3207
|
+
" }",
|
|
3186
3208
|
"}",
|
|
3187
3209
|
"if(issues.length){console.error('frontend-test checklist blocked: '+JSON.stringify(issues)); process.exit(1);}",
|
|
3188
3210
|
"console.log('frontend-test checklist ok cases='+manifest.cases.length+' source='+path.basename(manifestPath));",
|
|
@@ -3192,37 +3214,40 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3192
3214
|
"node -e",
|
|
3193
3215
|
JSON.stringify([
|
|
3194
3216
|
"const fs=require('fs'),path=require('path');",
|
|
3195
|
-
"const
|
|
3196
|
-
"const manifest
|
|
3217
|
+
"function fail(ruleId,detail){ const msg=JSON.stringify({ruleId:ruleId,detail:String(detail||'')}); console.error('frontend-test manifest blocked: '+msg); throw new Error('frontend-test manifest blocked: '+ruleId+(detail?(': '+detail):'')); }",
|
|
3218
|
+
"const draft='testcase/frontend/cases/manifest.draft.json',file='testcase/frontend/cases/manifest.json',tmp=file+'.tmp';",
|
|
3219
|
+
"if(!fs.existsSync(draft)) fail('draft-missing','missing '+draft);",
|
|
3220
|
+
"let manifest; try{manifest=JSON.parse(fs.readFileSync(draft,'utf8'));}catch(e){fail('draft-invalid-json',e&&e.message||e);}",
|
|
3221
|
+
"if(manifest.schemaVersion!==1) fail('draft-schema','schemaVersion must be 1');",
|
|
3222
|
+
"if(!Array.isArray(manifest.cases)||manifest.cases.length===0) fail('draft-empty-cases','cases must be a non-empty array');",
|
|
3223
|
+
`const maxCases=${maxCasesPerBatchLiteral};`,
|
|
3224
|
+
"if(manifest.cases.length>maxCases) fail('map-capacity-exceeded','cases='+manifest.cases.length+' exceeds maxCasesPerBatch/maxExpandedNodes='+maxCases+'; raise frontendTest.maxCasesPerBatch or shrink the suite');",
|
|
3197
3225
|
"const dims=new Set(['core','boundary','flow','backend']);",
|
|
3226
|
+
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
3198
3227
|
"const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
|
|
3228
|
+
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
3199
3229
|
"for(const c of manifest.cases){",
|
|
3200
|
-
" if(!c||typeof c.caseId!=='string'||!/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/.test(c.caseId)
|
|
3230
|
+
" if(!c||typeof c.caseId!=='string'||!/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/.test(c.caseId)) fail('case-id-shape','caseId must be FE-*, never AC-FE-*: '+String(c&&c.caseId));",
|
|
3231
|
+
" if(/^AC-/i.test(c.caseId)) fail('case-id-is-ac','caseId must not be an acceptance id: '+c.caseId);",
|
|
3232
|
+
" if(seen.has(c.caseId)) fail('duplicate-case-id',c.caseId);",
|
|
3201
3233
|
" seen.add(c.caseId);",
|
|
3202
|
-
" if(typeof c.dimension!=='string'||!dims.has(c.dimension))
|
|
3203
|
-
" if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim()))
|
|
3204
|
-
" for(const
|
|
3205
|
-
"
|
|
3206
|
-
"
|
|
3207
|
-
" if(
|
|
3208
|
-
" if(
|
|
3209
|
-
" if(
|
|
3234
|
+
" if(typeof c.dimension!=='string'||!dims.has(c.dimension)) fail('invalid-dimension',String(c.dimension));",
|
|
3235
|
+
" if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) fail('ac-mapping','invalid acIds for '+c.caseId);",
|
|
3236
|
+
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
3237
|
+
" c.casePath='testcase/frontend/cases/'+c.caseId+'.md';",
|
|
3238
|
+
" c.evidenceDir='testcase/frontend/evidence/'+c.caseId+'/';",
|
|
3239
|
+
" for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) fail('unsafe-path',k+': '+String(v)); }",
|
|
3240
|
+
" if(!fs.existsSync(c.casePath)) fail('case-file-missing','missing case file '+c.casePath+' (filename must equal caseId.md)');",
|
|
3241
|
+
" if(seenCasePath.has(c.casePath)) fail('duplicate-case-path',c.casePath); seenCasePath.add(c.casePath);",
|
|
3242
|
+
" if(seenEvidenceDir.has(c.evidenceDir)) fail('duplicate-evidence-dir',c.evidenceDir); seenEvidenceDir.add(c.evidenceDir);",
|
|
3210
3243
|
"}",
|
|
3211
|
-
"
|
|
3244
|
+
"const payload=JSON.stringify(manifest,null,2)+'\\n';",
|
|
3245
|
+
"fs.writeFileSync(tmp,payload); fs.renameSync(tmp,file); try{fs.unlinkSync(draft);}catch(_){}",
|
|
3212
3246
|
"process.stdout.write(JSON.stringify({cases:manifest.cases}));",
|
|
3213
3247
|
].join("")),
|
|
3214
3248
|
].join(" ");
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
JSON.stringify([
|
|
3218
|
-
"const fs=require('fs'),path=require('path');",
|
|
3219
|
-
"const manifestPath='testcase/frontend/cases/manifest.json';if(!fs.existsSync(manifestPath))throw new Error('missing '+manifestPath);",
|
|
3220
|
-
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));if(!Array.isArray(manifest.cases))throw new Error('invalid frontend case manifest');",
|
|
3221
|
-
"const statuses=new Set(['passed','failed','blocked']);let failed=false;",
|
|
3222
|
-
"for(const c of manifest.cases){const dir=c&&c.evidenceDir;if(!c||typeof c.caseId!=='string'||typeof dir!=='string'||path.isAbsolute(dir)||dir.includes('..')||!(dir==='testcase/frontend/evidence/'+c.caseId||dir.startsWith('testcase/frontend/evidence/'+c.caseId+'/'))){console.error('invalid case evidence target');failed=true;continue;}const execution=path.join(dir,'execution.md'),resultPath=path.join(dir,'case-result.json');if(!fs.existsSync(execution)){console.error(c.caseId+': missing '+execution);failed=true;}if(!fs.existsSync(resultPath)){console.error(c.caseId+': missing '+resultPath);failed=true;continue;}let result;try{result=JSON.parse(fs.readFileSync(resultPath,'utf8'));}catch{console.error(c.caseId+': invalid JSON '+resultPath);failed=true;continue;}if(!result||result.caseId!==c.caseId||!statuses.has(result.status)||!Array.isArray(result.evidencePaths)||result.evidencePaths.some(p=>typeof p!=='string'||path.isAbsolute(p)||p.includes('..'))){console.error(c.caseId+': result must have matching caseId, passed|failed|blocked status, and safe evidencePaths array');failed=true;continue;}if(result.status==='passed'&&(result.evidencePaths.length<1||!result.evidencePaths.some(p=>/\\.(png|jpg|jpeg|webp|zip|har|webm|mp4|md)$/i.test(p)))){console.error(c.caseId+': passed result requires browser evidence');failed=true;}if(result.status==='blocked'&&(typeof result.blockedReason!=='string'||!result.blockedReason.trim())){console.error(c.caseId+': blocked result requires blockedReason');failed=true;}}",
|
|
3223
|
-
"if(failed)process.exit(1);console.log('frontend case evidence validation ok cases='+manifest.cases.length);",
|
|
3224
|
-
].join("")),
|
|
3225
|
-
].join(" ");
|
|
3249
|
+
// Heal malformed/missing case evidence to blocked; only unsafe evidenceDir hard-fails.
|
|
3250
|
+
const evidenceValidation = buildFrontendCaseEvidenceValidateShellSnippet();
|
|
3226
3251
|
const frontendTestOutcomeGate = buildFrontendTestOutcomeGateShellSnippet();
|
|
3227
3252
|
const tasks = [
|
|
3228
3253
|
{
|
|
@@ -3236,12 +3261,12 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3236
3261
|
writeSet: ragWriteSet,
|
|
3237
3262
|
allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
|
|
3238
3263
|
forbiddenPaths: forbidden,
|
|
3239
|
-
outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md with machine-readable baseUrl and capability notes.",
|
|
3264
|
+
outputContract: "Write short testcase/frontend/rag/context.md and coverage-map.md with machine-readable baseUrl, baseUrlSource, environmentProbe=pending, and capability notes.",
|
|
3240
3265
|
subtask_prompt: [
|
|
3241
3266
|
"Build the frontend test RAG package (keep it short).",
|
|
3242
3267
|
"Read task source, routes/components/API or Mock facts, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
|
|
3243
|
-
"Prefer fixed fields: baseUrl, baseUrlSource, AC table, capability matrix (backend real/mock, pagination data, HTTP observation, error injection), risks, forbidden hosts. Do not paste large implementation dumps.",
|
|
3244
|
-
"Base URL resolution (required): (1) Prefer absolute http(s) frontend URL from task source config.md. (2) Else default http://localhost:5173. (3) Never production hosts. (4) Write `baseUrl: <url>` and `baseUrlSource: config.md|<path>|default-localhost-5173`. (5) Include exact start prefix: playwright-cli open --browser=chrome --headed <resolved-base-url>.",
|
|
3268
|
+
"Prefer fixed fields: baseUrl, baseUrlSource, environmentProbe, AC table, capability matrix (backend real/mock, pagination data, HTTP observation, error injection), risks, forbidden hosts. Do not paste large implementation dumps.",
|
|
3269
|
+
"Base URL resolution (required): (1) Prefer absolute http(s) frontend URL from task source config.md. (2) Else default http://localhost:5173. (3) Never production hosts. (4) Write `baseUrl: <url>` and `baseUrlSource: config.md|<path>|default-localhost-5173`. (5) Write `environmentProbe: pending` (preflight shell updates to reachable|unreachable|curl-unavailable with structured reason). (6) Include exact start prefix: playwright-cli open --browser=chrome --headed <resolved-base-url>.",
|
|
3245
3270
|
buildSourceContextBlock(sources),
|
|
3246
3271
|
].join("\n\n"),
|
|
3247
3272
|
},
|
|
@@ -3251,16 +3276,17 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3251
3276
|
role: "verifier",
|
|
3252
3277
|
executor: "shell",
|
|
3253
3278
|
complexity: "LOW",
|
|
3254
|
-
writePolicy: "
|
|
3279
|
+
writePolicy: "exclusive",
|
|
3280
|
+
writeSet: ragWriteSet,
|
|
3255
3281
|
allowedPaths: [...ragWriteSet],
|
|
3256
3282
|
forbiddenPaths: forbidden,
|
|
3257
|
-
outputContract: "Fail-closed preflight: absolute non-production baseUrl
|
|
3258
|
-
subtask_prompt: "
|
|
3283
|
+
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable (node ERROR so generate/map do not run).",
|
|
3284
|
+
subtask_prompt: "Parse frozen baseUrl from context.md (config.md preferred, else http://localhost:5173). Reject production / non-http(s). Probe with curl (HEAD then GET fallback; connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. 4xx/5xx/DNS/timeout/connection refused/TLS => blockedReason frontend-base-url-unreachable. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.",
|
|
3259
3285
|
shell: {
|
|
3260
3286
|
commands: [
|
|
3261
3287
|
[
|
|
3262
3288
|
"node -e",
|
|
3263
|
-
JSON.stringify("const fs=require('fs'); const p='testcase/frontend/rag/context.md'; if(!fs.existsSync(p))throw new Error('missing '+p);
|
|
3289
|
+
JSON.stringify("const fs=require('fs');const {spawnSync}=require('child_process');const p='testcase/frontend/rag/context.md';const probePath='testcase/frontend/rag/environment-probe.json';function writeProbe(obj){try{fs.mkdirSync('testcase/frontend/rag',{recursive:true});fs.writeFileSync(probePath,JSON.stringify(obj,null,2)+'\\n');let ctx=fs.existsSync(p)?fs.readFileSync(p,'utf8'):'';const line='environmentProbe: '+obj.status+(obj.blockedReason?(' ('+obj.blockedReason+')'):'');if(/environmentProbe\\s*[:=]/i.test(ctx)){ctx=ctx.replace(/environmentProbe\\s*[:=]\\s*.*/i,line);}else{ctx=ctx.trimEnd()+'\\n\\n'+line+'\\n';}fs.writeFileSync(p,ctx);}catch(e){console.error('probe-write-failed',e&&e.message||e);}}function redactUrl(u){try{const x=new URL(u);x.username='';x.password='';if(x.search){x.search='';}return x.toString();}catch(_){return String(u).replace(/\\/\\/[^@\\s]+@/g,'//');}}function failBlocked(reason,extra){const payload=Object.assign({status:'unreachable',blockedReason:reason,baseUrlRedacted:extra&&extra.baseUrlRedacted||null,httpStatus:extra&&extra.httpStatus||null,method:extra&&extra.method||null,curlExit:extra&&extra.curlExit||null,errorClass:extra&&extra.errorClass||null},extra||{});writeProbe(payload);console.error('frontend-test preflight blocked: '+JSON.stringify({blockedReason:reason,baseUrl:payload.baseUrlRedacted,httpStatus:payload.httpStatus,errorClass:payload.errorClass}));throw new Error('frontend-test preflight blocked: '+reason);}if(!fs.existsSync(p))throw new Error('missing '+p);const s=fs.readFileSync(p,'utf8');const patterns=[/baseUrl\\s*[:=]\\s*(https?:\\/\\/\\S+)/i,/base[- ]url\\s*[:=]\\s*(https?:\\/\\/\\S+)/i,/playwright-cli open --browser=chrome --headed\\s+(https?:\\/\\/\\S+)/i,/(https?:\\/\\/(?:localhost|127\\.0\\.0\\.1)[^\\s)\\}\\],\\\"']*)/i];let baseUrl=null;for(const re of patterns){const m=s.match(re);if(m){baseUrl=m[1];break;}}if(!baseUrl)throw new Error('frontend-test preflight missing absolute baseUrl (prefer config.md; default http://localhost:5173)');baseUrl=baseUrl.replace(/[)\\}\\],.\\\"']+$/,'');if(!/^https?:\\/\\//i.test(baseUrl))throw new Error('baseUrl must be absolute http(s): '+baseUrl);if(/(?:^|\\/\\/)(?:www\\.)?[^\\s\\/]*(?:prod|production)/i.test(baseUrl))throw new Error('production URL forbidden: '+baseUrl);const safe=redactUrl(baseUrl);const curlCheck=spawnSync('curl',['--version'],{encoding:'utf8'});if(curlCheck.error||curlCheck.status!==0){failBlocked('curl-unavailable',{baseUrlRedacted:safe,errorClass:'curl-missing'});}function probe(method){const args=['-sS','-o','/dev/null','-w','%{http_code}','--connect-timeout','3','--max-time','8','-X',method,'-L','--max-redirs','3','--http1.1','--proto-redir','=http,https',safe];const r=spawnSync('curl',args,{encoding:'utf8'});return r;}let used='HEAD';let r=probe('HEAD');let code=String(r.stdout||'').trim();let statusNum=parseInt(code,10);const headRejected=r.status!==0||!statusNum||statusNum===405||statusNum===501;if(headRejected){used='GET';r=probe('GET');code=String(r.stdout||'').trim();statusNum=parseInt(code,10);}const ok=statusNum>=200&&statusNum<400;if(!ok){const errClass=r.error?'spawn-error':(r.status!==0?'curl-exit-'+r.status:('http-'+statusNum));failBlocked('frontend-base-url-unreachable',{baseUrlRedacted:safe,httpStatus:statusNum||null,method:used,curlExit:r.status,errorClass:errClass});}writeProbe({status:'reachable',blockedReason:null,baseUrlRedacted:safe,httpStatus:statusNum,method:used,curlExit:r.status});console.log('frontend-test-execution-v1 validated baseUrl='+safe+' probe=reachable method='+used+' httpStatus='+statusNum);"),
|
|
3264
3290
|
].join(" "),
|
|
3265
3291
|
],
|
|
3266
3292
|
cwd: ".",
|
|
@@ -3282,10 +3308,18 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3282
3308
|
subtask_prompt: [
|
|
3283
3309
|
"Use skill playwright-cli-case-generator.",
|
|
3284
3310
|
"Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and existing testcase/frontend/cases/. Write only testcase/frontend/cases/**.",
|
|
3285
|
-
"Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).
|
|
3286
|
-
"
|
|
3287
|
-
"
|
|
3311
|
+
"Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
|
|
3312
|
+
"HARD ID CONTRACT (do not confuse these):",
|
|
3313
|
+
"- caseId / filename MUST be FE-<FEATURE>-<NNN>-<dimension> (example FE-LOGIN-001-core). NEVER use AC-FE-* as caseId or filename.",
|
|
3314
|
+
"- acIds MUST list acceptance criteria only: AC-FE-* / AC-* from the declared task list (example AC-FE-001). NEVER put FE-* case ids into acIds.",
|
|
3315
|
+
"- casePath MUST equal testcase/frontend/cases/<caseId>.md; evidenceDir MUST equal testcase/frontend/evidence/<caseId>/. Materialize will rewrite paths, but files must already use caseId filenames.",
|
|
3316
|
+
`Declared acceptance ids for this task (use only these in acIds when non-empty): ${declaredAcIds.length > 0 ? declaredAcIds.join(", ") : "(none extracted - still use AC-* shape, never FE-* case ids)"}.`,
|
|
3317
|
+
"dimensions: core|boundary|flow|backend only.",
|
|
3318
|
+
"Prefer a small smoke suite (default max roughly 4-8 cases unless task frontendTest.maxCasesPerBatch is higher). Never invent unavailable API fields or credentials. Do not create pytest or Playwright source.",
|
|
3319
|
+
"HARD playwright-cli-only: every browser step must use repo skill playwright-cli declared commands only. Forbidden: bare `playwright`, `npx playwright`, `playwright test`, `@playwright/test`, Node Playwright API, or generating Playwright/Pytest source. No fallback when playwright-cli is unavailable - case must instruct blocked evidence playwright-cli-unavailable.",
|
|
3320
|
+
"Copy the resolved absolute baseUrl from context.md (baseUrl field; resolved from config.md or default http://localhost:5173). Every browser start command must be: playwright-cli open --browser=chrome --headed <resolved-base-url-from-context.md> with that concrete URL - never leave a <base-url> placeholder. Use default browser session only; never write -s=<case-id>.",
|
|
3288
3321
|
"Each case must be independently reproducible with fixture/reset, UI reset, snapshot-before-ref, evidence write point under testcase/frontend/evidence/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.",
|
|
3322
|
+
"U/D data ownership (required for modify/delete): (1) Only mutate data whose ownership is proven by current login identity + observable UI/API owner fields - never by name/guessed id/list order alone. (2) If current user has no data, create tagged cleanable data in current-user context, then U/D, then cleanup+verify. (3) Else only task-authorized Mock, labeled as Mock (not real backend proof). (4) If ownership unverifiable and create/Mock unavailable: write blocked with blockedReason current-user-data-unavailable | data-ownership-unverifiable | safe-test-data-setup-unavailable - do not risk cross-user data. (5) Never touch other users, shared fixtures, production, or non-cleanable data.",
|
|
3289
3323
|
].join("\n\n"),
|
|
3290
3324
|
},
|
|
3291
3325
|
];
|
|
@@ -3368,7 +3402,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3368
3402
|
writePolicy: "read-only",
|
|
3369
3403
|
allowedPaths: [...ragWriteSet, ...casesWriteSet],
|
|
3370
3404
|
forbiddenPaths: forbidden,
|
|
3371
|
-
outputContract: "Mechanical checklist: open-prefix, non-prod absolute URL, acIds, no pytest/playwright test source; emit structured ruleId issues on failure.",
|
|
3405
|
+
outputContract: "Mechanical checklist: open-prefix, non-prod absolute URL, acIds, playwright-cli-only, no pytest/playwright test source; emit structured ruleId issues on failure.",
|
|
3372
3406
|
subtask_prompt: "Scan generated cases/manifest against the shared blocking checklist. Do not use free-form LLM verdicts.",
|
|
3373
3407
|
shell: { commands: [checklistValidation], cwd: ".", timeoutMs: 120000 },
|
|
3374
3408
|
}, {
|
|
@@ -3381,7 +3415,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3381
3415
|
writeSet: casesWriteSet,
|
|
3382
3416
|
allowedPaths: casesWriteSet,
|
|
3383
3417
|
forbiddenPaths: forbidden,
|
|
3384
|
-
outputContract: "Validated frontend manifest payload { cases: [...] }; atomically materialize testcase/frontend/cases/manifest.json
|
|
3418
|
+
outputContract: "Validated frontend manifest payload { cases: [...] }; ruleId-tagged fail-closed validation; atomically materialize testcase/frontend/cases/manifest.json via temp+rename then delete draft; stdout is exactly one final JSON line {cases}.",
|
|
3385
3419
|
subtask_prompt: "Validate manifest.draft.json and materialize manifest.json after the mechanical checklist (and optional blocking review) passes.",
|
|
3386
3420
|
shell: { commands: [manifestValidation], cwd: ".", timeoutMs: 120000 },
|
|
3387
3421
|
}, {
|
|
@@ -3427,9 +3461,9 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3427
3461
|
writeSet: [`${evidenceRoot}/{{case.caseId}}/**`],
|
|
3428
3462
|
outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, and tokens.",
|
|
3429
3463
|
subtaskPromptTemplate: [
|
|
3430
|
-
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new).
|
|
3431
|
-
"1) Read baseUrl from testcase/frontend/rag/context.md (config.md preferred, else http://localhost:5173). 2) Start browser: playwright-cli open --browser=chrome --headed <resolved-base-url> (default session only; no -s=). 3) Follow
|
|
3432
|
-
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json
|
|
3464
|
+
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
|
|
3465
|
+
"1) Read baseUrl from testcase/frontend/rag/context.md (config.md preferred, else http://localhost:5173). 2) Start browser ONLY: playwright-cli open --browser=chrome --headed <resolved-base-url> (default session only; no -s=). 3) Follow case steps with snapshot before element refs using only playwright-cli skill commands. 4) If env/CLI/baseUrl/playwright-cli unavailable, write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable) and do not open a browser. 5) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
3466
|
+
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
|
|
3433
3467
|
"Business failed/blocked is a recorded result, not a node failure. Close browser. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
3434
3468
|
].join("\n\n"),
|
|
3435
3469
|
},
|
|
@@ -3440,11 +3474,12 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3440
3474
|
role: "verifier",
|
|
3441
3475
|
executor: "shell",
|
|
3442
3476
|
complexity: "LOW",
|
|
3443
|
-
writePolicy: "
|
|
3477
|
+
writePolicy: "exclusive",
|
|
3478
|
+
writeSet: [`${evidenceRoot}/**`],
|
|
3444
3479
|
allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
|
|
3445
3480
|
forbiddenPaths: forbidden,
|
|
3446
|
-
outputContract: "Deterministic
|
|
3447
|
-
subtask_prompt: "Validate
|
|
3481
|
+
outputContract: "Deterministic evidence gate: heal missing/malformed case-result to blocked(invalid-evidence-shape); hard-fail only on unsafe evidenceDir. Does not block retrospect.",
|
|
3482
|
+
subtask_prompt: "Validate frontend case evidence before result materialization. Prefer healing bad shapes to blocked so pipeline can still produce a report; only path-escape failures abort the node.",
|
|
3448
3483
|
shell: { commands: [evidenceValidation], cwd: ".", timeoutMs: 120000 },
|
|
3449
3484
|
}, {
|
|
3450
3485
|
id: "materialize-frontend-test-result-shell",
|
|
@@ -3509,6 +3544,9 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3509
3544
|
"Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
|
|
3510
3545
|
"Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
|
|
3511
3546
|
"Browser startup for generated cases must be playwright-cli open --browser=chrome --headed <resolved-base-url>; resolve baseUrl from task source config.md when present, otherwise default http://localhost:5173; generated operations stay in the default browser session and must not use unverified named-session flags.",
|
|
3547
|
+
"playwright-cli-only: generators and executors may call only skill-declared playwright-cli commands; bare playwright / npx playwright / @playwright/test / Playwright source are forbidden with no native Playwright fallback.",
|
|
3548
|
+
"Environment preflight must curl-probe the frozen non-production baseUrl before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
|
|
3549
|
+
"U/D cases must prove current-user data ownership or create cleanable current-user data or authorized Mock; otherwise blocked (current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable) without cross-user mutation.",
|
|
3512
3550
|
"Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
|
|
3513
3551
|
"Pipeline acceptance for frontend-test is the final retrospect report under testcase/frontend/reports/; case pass rate and outcome=passed are quality signals, not the default pipeline success condition.",
|
|
3514
3552
|
blockingReview
|
|
@@ -4598,9 +4636,11 @@ function applyDefaultReadOnlyRetryPolicy(spec) {
|
|
|
4598
4636
|
for (const task of spec.tasks) {
|
|
4599
4637
|
if (task.retryPolicy !== undefined)
|
|
4600
4638
|
continue;
|
|
4601
|
-
if (isSafeReadOnlyPiRetryCandidate(task))
|
|
4602
|
-
|
|
4603
|
-
|
|
4639
|
+
if (!isSafeReadOnlyPiRetryCandidate(task))
|
|
4640
|
+
continue;
|
|
4641
|
+
task.retryPolicy = task.outputProtocol
|
|
4642
|
+
? PROTOCOL_AWARE_PI_RETRY_POLICY
|
|
4643
|
+
: DEFAULT_READ_ONLY_PI_RETRY_POLICY;
|
|
4604
4644
|
}
|
|
4605
4645
|
}
|
|
4606
4646
|
function getTaskOrThrow(spec, id) {
|
|
@@ -4634,6 +4674,7 @@ function buildReviewNode(sources) {
|
|
|
4634
4674
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
4635
4675
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
4636
4676
|
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; Critical/Important findings force request-revision. No file writes.",
|
|
4677
|
+
outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
|
|
4637
4678
|
subtask_prompt: [
|
|
4638
4679
|
"Review upstream implementation and verification evidence.",
|
|
4639
4680
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
@@ -4661,6 +4702,7 @@ function buildReviewVerdictRecoveryNode(sources) {
|
|
|
4661
4702
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
4662
4703
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
4663
4704
|
outputContract: "Plain Markdown whose first non-empty line is exactly VERDICT: pass or VERDICT: request-revision, followed by the original review findings without substantive changes. No file writes.",
|
|
4705
|
+
outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
|
|
4664
4706
|
subtask_prompt: [
|
|
4665
4707
|
"Normalize the output format of review-pi; this is the single read-only format-recovery attempt for the review verdict protocol.",
|
|
4666
4708
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|