@tea-agent/loop-agent 0.38.3 → 0.38.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +153 -96
- package/README.md +9 -5
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +428 -27
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +18 -9
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/shell-executor.js +209 -13
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/operator-actions.js +116 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +36 -5
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-pm3c2Rsl.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
- package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
- package/dist/worker/console/static/assets/{index-Ya5FE7cD.css → index-C8rOIAJr.css} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-DEbyrEfW.js} +71 -71
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-CAyXjQjN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-CcWKn6tS.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/observe/node-input.js +2 -0
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +11 -0
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +561 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +60 -4
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/failure-category.js +8 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +5 -1
- package/dist/workflows/dag/frontend-repair.js +14 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +151 -11
- package/dist/workflows/dag/frontend-verification-trace.js +29 -4
- package/dist/workflows/dag/init-hybrid.js +125 -24
- package/dist/workflows/dag/node-execution.js +20 -2
- package/dist/workflows/dag/rerun-plan.js +112 -0
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +91 -0
- package/dist/workflows/dag/types.js +9 -1
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +2 -2
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -11,7 +11,7 @@ import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
|
11
11
|
import { extractTaskSourceOpenspecPaths } from "../../shared/openspec-spec.js";
|
|
12
12
|
import { BASELINE_FORBIDDEN_PATHS } from "./governance-constants.js";
|
|
13
13
|
import { buildDecisionEnvelopePromptContract } from "./decision-envelope.js";
|
|
14
|
-
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, isSafeReadOnlyPiRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
14
|
+
import { DEFAULT_READ_ONLY_PI_RETRY_POLICY, PLANNER_OUTPUT_LIMIT_RETRY_POLICY, PROTOCOL_AWARE_PI_RETRY_POLICY, STRUCTURED_REQUIRED_PI_RETRY_POLICY, BACKEND_TEST_WRITER_COMPLETENESS_RETRY_POLICY, WRITER_TRANSPORT_RETRY_POLICY, TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE, TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY, isCanonicalFinalVerifyShellRetryCandidate, isSafeReadOnlyPiRetryCandidate, isTargetTemplateImplementPi, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
15
15
|
import { REVIEW_JSON_VERDICT_OUTPUT_PROTOCOL, REVIEW_VERDICT_OUTPUT_PROTOCOL, } from "./output-protocol.js";
|
|
16
16
|
import { resolveAdapter } from "../../adapters/index.js";
|
|
17
17
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
@@ -34,6 +34,7 @@ import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result
|
|
|
34
34
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
35
35
|
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
36
36
|
import { buildFrontendImplementationContractSkeleton, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
|
|
37
|
+
import { FRONTEND_NO_VERIFICATION_MARKER_TEXT } from "./frontend-verification-trace.js";
|
|
37
38
|
import { serializeDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
38
39
|
const REQUIREMENT_FILE = "需求.md";
|
|
39
40
|
const CONSTRAINT_FILE = "执行约束.md";
|
|
@@ -1281,11 +1282,15 @@ function deriveParallelScoutPaths(taskConfig) {
|
|
|
1281
1282
|
: allowed,
|
|
1282
1283
|
};
|
|
1283
1284
|
}
|
|
1285
|
+
const FRONTEND_NO_STATIC_VERIFICATION_MARKER = `node -e "console.log('${FRONTEND_NO_VERIFICATION_MARKER_TEXT}; static/behavior verification not-run')"`;
|
|
1284
1286
|
/**
|
|
1285
1287
|
* Resolve frontend verification fallbacks from the target project's own
|
|
1286
1288
|
* package scripts. The DAG builder is also used by unit fixtures without a
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1289
|
+
* repoRoot, so those fixtures retain the historical generic fallback. A real
|
|
1290
|
+
* project (repoRoot set) without a readable package.json has no scripts to
|
|
1291
|
+
* invoke; the generic fallback would only inject commands guaranteed to fail
|
|
1292
|
+
* at verify time, so such projects fall through to the tsc probe and may end
|
|
1293
|
+
* up with no fallback commands plus a generation-time advisory.
|
|
1289
1294
|
*/
|
|
1290
1295
|
async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
1291
1296
|
const genericFallback = {
|
|
@@ -1302,10 +1307,8 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
|
|
|
1302
1307
|
}
|
|
1303
1308
|
}
|
|
1304
1309
|
catch {
|
|
1305
|
-
|
|
1310
|
+
// No readable manifest: leave scripts unset and keep probing.
|
|
1306
1311
|
}
|
|
1307
|
-
if (!scripts)
|
|
1308
|
-
return genericFallback;
|
|
1309
1312
|
let packageManager = "npm";
|
|
1310
1313
|
for (const [lockfile, manager] of [
|
|
1311
1314
|
["pnpm-lock.yaml", "pnpm"],
|
|
@@ -1382,19 +1385,66 @@ function toDagSourcePath(sources, absolutePath) {
|
|
|
1382
1385
|
absolutePath,
|
|
1383
1386
|
});
|
|
1384
1387
|
}
|
|
1385
|
-
|
|
1388
|
+
/**
|
|
1389
|
+
* Declaration-line shapes that assert a requirement id: list items
|
|
1390
|
+
* ("- AC-1: ..."), numbered/ordered acceptance entries ("1. AC-1 ...",
|
|
1391
|
+
* "1、AC-1"), table rows ("| AC-1 | ..."), and explicit key/value or
|
|
1392
|
+
* heading declarations ("AC-1:...", "id: AC-1"). Ids that merely appear
|
|
1393
|
+
* inside narrative prose (e.g. a fixture run id mentioned in background:
|
|
1394
|
+
* "夹具 run 2026-08-23-req-f03535aa 已存在") are incidental mentions, not
|
|
1395
|
+
* declared requirements, and must not pollute sourceBinding.requirementIds.
|
|
1396
|
+
*/
|
|
1397
|
+
const REQUIREMENT_ID_DECLARATION_LINE = /^\s*(?:[-+*]\s+|\d+[.、))]\s*|\|\s*)?(?:[-A-Z0-9]+\s*[::]\s*)?(?:\*\*)?(?:REQ|BR|AC)-/i;
|
|
1398
|
+
function isRequirementIdDeclarationLine(line) {
|
|
1399
|
+
return REQUIREMENT_ID_DECLARATION_LINE.test(line);
|
|
1400
|
+
}
|
|
1401
|
+
function extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown) {
|
|
1386
1402
|
const ids = [];
|
|
1387
1403
|
const seen = new Set();
|
|
1388
|
-
|
|
1404
|
+
const collect = (markdown, declarationsOnly) => {
|
|
1389
1405
|
if (!markdown)
|
|
1390
|
-
|
|
1391
|
-
|
|
1406
|
+
return;
|
|
1407
|
+
const source = declarationsOnly
|
|
1408
|
+
? markdown
|
|
1409
|
+
.split(/\r?\n/)
|
|
1410
|
+
.filter((line) => isRequirementIdDeclarationLine(line))
|
|
1411
|
+
.join("\n")
|
|
1412
|
+
: markdown;
|
|
1413
|
+
for (const match of source.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1392
1414
|
const id = match[0].toUpperCase();
|
|
1393
1415
|
if (!seen.has(id)) {
|
|
1394
1416
|
seen.add(id);
|
|
1395
1417
|
ids.push(id);
|
|
1396
1418
|
}
|
|
1397
1419
|
}
|
|
1420
|
+
};
|
|
1421
|
+
// The requirement document itself is human-authored acceptance prose:
|
|
1422
|
+
// only declaration lines declare requirements there. If the document has
|
|
1423
|
+
// no declaration-shaped lines at all (minimal free-form tasks like
|
|
1424
|
+
// "covers AC-001"), fall back to its full text so bindings never go
|
|
1425
|
+
// missing for unstructured input. References and constraints are bound
|
|
1426
|
+
// documents (acceptance yaml, analysis docs) whose ids are authoritative
|
|
1427
|
+
// wherever they appear — keep full scan for them.
|
|
1428
|
+
const declarationLines = requirementMarkdown
|
|
1429
|
+
.split(/\r?\n/)
|
|
1430
|
+
.filter((line) => isRequirementIdDeclarationLine(line));
|
|
1431
|
+
collect(requirementMarkdown, declarationLines.length > 0);
|
|
1432
|
+
for (const markdown of fallbackMarkdown) {
|
|
1433
|
+
if (markdown)
|
|
1434
|
+
collect(markdown, false);
|
|
1435
|
+
}
|
|
1436
|
+
return ids;
|
|
1437
|
+
}
|
|
1438
|
+
/** Extract ids from an explicit authoritative section (full scan inside it). */
|
|
1439
|
+
function extractSectionRequirementIds(sectionMarkdown) {
|
|
1440
|
+
const ids = [];
|
|
1441
|
+
const seen = new Set();
|
|
1442
|
+
for (const match of sectionMarkdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
1443
|
+
const id = match[0].toUpperCase();
|
|
1444
|
+
if (!seen.has(id)) {
|
|
1445
|
+
seen.add(id);
|
|
1446
|
+
ids.push(id);
|
|
1447
|
+
}
|
|
1398
1448
|
}
|
|
1399
1449
|
return ids;
|
|
1400
1450
|
}
|
|
@@ -1408,7 +1458,7 @@ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallback
|
|
|
1408
1458
|
// Without it, preserve legacy full-scan behavior (requirement + references).
|
|
1409
1459
|
const sectionMatch = requirementMarkdown.match(/(?:^|\n)##\s*Acceptance References\s*\n([\s\S]*?)(?=\n##\s+|\n#\s+|$)/i);
|
|
1410
1460
|
if (sectionMatch?.[1]) {
|
|
1411
|
-
const fromSection =
|
|
1461
|
+
const fromSection = extractSectionRequirementIds(sectionMatch[1]);
|
|
1412
1462
|
if (fromSection.length > 0)
|
|
1413
1463
|
return fromSection;
|
|
1414
1464
|
}
|
|
@@ -1719,6 +1769,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1719
1769
|
forbiddenPaths,
|
|
1720
1770
|
outputContract: "Archived final shell verification stdout/stderr with exit codes; no worktree writes.",
|
|
1721
1771
|
subtask_prompt: "Run the adapter-resolved final verification commands before read-only verification review.",
|
|
1772
|
+
transientRetryProfile: TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE,
|
|
1722
1773
|
shell: {
|
|
1723
1774
|
commands: verifyShellCommands,
|
|
1724
1775
|
verifyEvidence: buildVerifyEvidence({
|
|
@@ -1839,6 +1890,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1839
1890
|
allowedPaths: implementPaths.allowedPaths,
|
|
1840
1891
|
forbiddenPaths,
|
|
1841
1892
|
writerOutcomePolicy: { type: "implementation-outcome-v1" },
|
|
1893
|
+
transientRetryProfile: TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE,
|
|
1842
1894
|
subtask_prompt: [
|
|
1843
1895
|
"Implement the approved plan with minimal focused changes.",
|
|
1844
1896
|
"Stay within writeSet. Do not write root artifacts/** unless artifacts paths are explicitly declared in writeSet.",
|
|
@@ -1898,6 +1950,7 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
1898
1950
|
],
|
|
1899
1951
|
};
|
|
1900
1952
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
1953
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
1901
1954
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
1902
1955
|
parseDagSpec(spec);
|
|
1903
1956
|
assertValidDagSpec(spec);
|
|
@@ -2527,7 +2580,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2527
2580
|
return buildBlockedFrontendMockDag(frontendSources, readOnlyPaths, forbiddenPaths, globalConstraints, blockedReason);
|
|
2528
2581
|
}
|
|
2529
2582
|
const fallbackVerifyCommands = await discoverFrontendFallbackVerifyCommands(sources.repoRoot);
|
|
2530
|
-
|
|
2583
|
+
// The verification bundle schema requires at least one static command. When
|
|
2584
|
+
// a real project exposes no usable verification command at all, run an
|
|
2585
|
+
// explicit no-op marker instead of a command that is guaranteed to fail:
|
|
2586
|
+
// the trace then records not-run honestly and the advisory asks the task
|
|
2587
|
+
// to declare verification commands and regenerate.
|
|
2588
|
+
const staticFallbackCommands = fallbackVerifyCommands.staticCommands.length > 0
|
|
2589
|
+
? fallbackVerifyCommands.staticCommands
|
|
2590
|
+
: [FRONTEND_NO_STATIC_VERIFICATION_MARKER];
|
|
2531
2591
|
const behaviorFallbackCommands = fallbackVerifyCommands.behaviorCommands;
|
|
2532
2592
|
const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
|
|
2533
2593
|
repoRoot: sources.repoRoot,
|
|
@@ -2626,6 +2686,11 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2626
2686
|
...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
|
|
2627
2687
|
].join("\n");
|
|
2628
2688
|
const advisories = [];
|
|
2689
|
+
if (!hasDeclaredFrontendVerification &&
|
|
2690
|
+
fallbackVerifyCommands.staticCommands.length === 0 &&
|
|
2691
|
+
fallbackVerifyCommands.behaviorCommands.length === 0) {
|
|
2692
|
+
advisories.push("未发现可用的前端验证命令:目标项目没有可读取的 package.json scripts,也未探测到本地 TypeScript,verify 节点将没有静态/行为命令可执行。请在 task.json --verify 或执行约束.md 的验证约束中显式声明命令(例如 node --check src/app.js),然后重新生成 DAG。");
|
|
2693
|
+
}
|
|
2629
2694
|
if (frontendSourceMentionsMock(frontendSources) &&
|
|
2630
2695
|
frontendMockStrategyMustBeNotNeeded(frontendSources)) {
|
|
2631
2696
|
advisories.push("auto 模式已将 Mock 策略收窄为 not-needed:任务源提到接口/API/Mock 需求,但仓库无确认 Mock 能力或无确定性 Mock 验证命令。若项目规范要求 Mock,请声明 frontendMock.verifyCommands 或 policy:required 后重新生成 DAG。");
|
|
@@ -4759,6 +4824,13 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4759
4824
|
};
|
|
4760
4825
|
const declaredRequirementIds = buildDagSourceBinding(sources).requirementIds;
|
|
4761
4826
|
const declaredAcIds = declaredRequirementIds.filter((id) => /^AC(?:-[A-Z0-9]+)+$/i.test(id));
|
|
4827
|
+
// Optional project-local UI anchor ledger: only mention it in prompts when it
|
|
4828
|
+
// exists so ledger-less projects keep generating without noise.
|
|
4829
|
+
const hasUiAnchorsLedger = Boolean(sources.repoRoot &&
|
|
4830
|
+
existsSync(path.join(sources.repoRoot, "testcase/frontend/rag/ui-anchors.md")));
|
|
4831
|
+
const uiAnchorsLedgerInstruction = hasUiAnchorsLedger
|
|
4832
|
+
? "Also read testcase/frontend/rag/ui-anchors.md (UI anchor ledger). Every passing find assertion in generated cases must quote a ledger row whose 状态 is 有效 for the matching page x state section; rows marked 不可断言/失效 must not be used as passing assertions. When an AC names a control absent from the ledger section for that state, emit a blocked case note (blockedReason unique-control-unavailable) instead of exploratory find steps, and follow ledger 备注 alternatives (split-node short literals) exactly."
|
|
4833
|
+
: "";
|
|
4762
4834
|
const reviewMode = config.reviewMode;
|
|
4763
4835
|
const blockingReview = reviewMode === "blocking";
|
|
4764
4836
|
const strictOutcomeGate = config.strictOutcomeGate;
|
|
@@ -4799,6 +4871,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4799
4871
|
"const caseIdRe=/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/;",
|
|
4800
4872
|
,
|
|
4801
4873
|
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
4874
|
+
"const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
|
|
4875
|
+
"const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
|
|
4802
4876
|
"for(const c of manifest.cases){",
|
|
4803
4877
|
" const id=c&&c.caseId||'?';",
|
|
4804
4878
|
" 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-*'});",
|
|
@@ -4817,6 +4891,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4817
4891
|
" if(typeof ac!=='string'){issues.push({ruleId:'ac-id-shape',caseId:id,detail:String(ac)+' must look like AC-FE-001'});continue;}",
|
|
4818
4892
|
" if(/^FE-/i.test(ac)){issues.push({ruleId:'ac-id-is-case',caseId:id,detail:ac+' looks like caseId; acIds must be AC-*'});continue;}",
|
|
4819
4893
|
" if(!acIdRe.test(ac)){issues.push({ruleId:'ac-id-shape',caseId:id,detail:ac+' must look like AC-FE-001'});continue;}",
|
|
4894
|
+
" if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))){issues.push({ruleId:'ac-id-truncated',caseId:id,detail:ac+' is a truncated acceptance id (missing feature prefix or sequence number)'});continue;}",
|
|
4820
4895
|
" if(declaredAc.size>0&&!declaredAc.has(ac))issues.push({ruleId:'unknown-ac',caseId:id,detail:ac+' not in task sourceBinding.requirementIds'});",
|
|
4821
4896
|
" }",
|
|
4822
4897
|
" }",
|
|
@@ -4848,6 +4923,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4848
4923
|
`const declaredAc=new Set(${declaredAcIdsLiteral});`,
|
|
4849
4924
|
"const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
|
|
4850
4925
|
"const acIdRe=/^AC(?:-[A-Z0-9]+)+$/i;",
|
|
4926
|
+
"const truncatedAcRe=/^AC-FE-[A-Z]+$/i;",
|
|
4927
|
+
"const truncatedUndeclaredAcRe=/^(?:AC-\\d{3}|AC-FE-[A-Z]+)$/i;",
|
|
4851
4928
|
"for(const c of manifest.cases){",
|
|
4852
4929
|
" 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));",
|
|
4853
4930
|
" if(/^AC-/i.test(c.caseId)) fail('case-id-is-ac','caseId must not be an acceptance id: '+c.caseId);",
|
|
@@ -4855,7 +4932,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4855
4932
|
" seen.add(c.caseId);",
|
|
4856
4933
|
" if(typeof c.dimension!=='string'||!dims.has(c.dimension)) fail('invalid-dimension',String(c.dimension));",
|
|
4857
4934
|
" 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);",
|
|
4858
|
-
" 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'); }",
|
|
4935
|
+
" 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(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))) fail('ac-id-truncated','acIds entry is a truncated acceptance id (missing feature prefix or sequence number): '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
4859
4936
|
" c.casePath='testcase/frontend/cases/'+c.caseId+'.md';",
|
|
4860
4937
|
" c.evidenceDir='testcase/frontend/evidence/'+c.caseId+'/';",
|
|
4861
4938
|
" 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)); }",
|
|
@@ -4980,6 +5057,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
4980
5057
|
subtask_prompt: [
|
|
4981
5058
|
"Use skill playwright-cli-case-generator.",
|
|
4982
5059
|
"Read testcase/frontend/rag/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.",
|
|
5060
|
+
...(uiAnchorsLedgerInstruction ? [uiAnchorsLedgerInstruction] : []),
|
|
4983
5061
|
"Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and the draft case paths testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json. Write only those same draft paths. Do not write testcase/frontend/cases/manifest.json.",
|
|
4984
5062
|
"Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
|
|
4985
5063
|
"HARD ID CONTRACT (do not confuse these):",
|
|
@@ -5139,7 +5217,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5139
5217
|
subtaskPromptTemplate: [
|
|
5140
5218
|
"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.",
|
|
5141
5219
|
"Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
|
|
5142
|
-
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
5220
|
+
"1) Read the concrete baseUrl from testcase/frontend/rag/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal `eX`/`eY`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. Unique-control early stop: when an AC names a specific control that is absent from the first post-navigation snapshot after reaching the required state (and no setup-required gate appeared), do ONE find with the case's literal as evidence; if it returns 0 matches, record status=blocked with blockedReason unique-control-unavailable citing that single find - do NOT re-explore with alternative literals, repeated snapshots, navigation detours, or rerun loops. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.",
|
|
5143
5221
|
"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).",
|
|
5144
5222
|
"Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
5145
5223
|
].join("\n\n"),
|
|
@@ -5162,7 +5240,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5162
5240
|
"const manifestPath='testcase/frontend/cases/manifest.json';",
|
|
5163
5241
|
"if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
|
|
5164
5242
|
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
|
|
5165
|
-
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
5243
|
+
"for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(r.status==='failed')reason='failed-retry';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||reason==='failed-retry'||missing)&&attempt<" + String(round) + ";if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
|
|
5166
5244
|
`fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
|
|
5167
5245
|
].join("");
|
|
5168
5246
|
tasks.push({
|
|
@@ -5230,6 +5308,7 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
5230
5308
|
"RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite the authoritative execution.md and case-result.json; its final status replaces the earlier case result.",
|
|
5231
5309
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
|
|
5232
5310
|
"Read the concrete baseUrl from testcase/frontend/rag/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.",
|
|
5311
|
+
"Unique-control early stop: if the first post-navigation snapshot lacks the control an AC names (and no setup-required gate appeared), one find returning 0 matches is sufficient evidence - write status=blocked blockedReason unique-control-unavailable instead of exploratory retries.",
|
|
5233
5312
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}. Write fixed sections `### 执行摘要` and `### 实际执行步骤` to execution.md when available.",
|
|
5234
5313
|
].join("\n\n"),
|
|
5235
5314
|
},
|
|
@@ -6442,7 +6521,12 @@ async function buildHybridDagForTemplate(sources, template, options = {}) {
|
|
|
6442
6521
|
spec.sourceBinding = buildDagSourceBinding(sources);
|
|
6443
6522
|
spec.taskContractBinding = taskContractBinding;
|
|
6444
6523
|
assertNoGovernanceFlagOnDisallowedTemplate(spec, template);
|
|
6445
|
-
|
|
6524
|
+
if (template === "standard-dag" ||
|
|
6525
|
+
template === "review-gated-dag" ||
|
|
6526
|
+
template === "supervised-implementation") {
|
|
6527
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
6528
|
+
}
|
|
6529
|
+
spec = parseDagSpec(spec);
|
|
6446
6530
|
assertValidDagSpec(spec);
|
|
6447
6531
|
return spec;
|
|
6448
6532
|
}
|
|
@@ -6493,14 +6577,6 @@ export async function buildHybridDagFromTask(sources, options = {}) {
|
|
|
6493
6577
|
function cloneTask(task, patch = {}) {
|
|
6494
6578
|
return { ...task, ...patch };
|
|
6495
6579
|
}
|
|
6496
|
-
/**
|
|
6497
|
-
* Apply default Pi retry policies to generated DAG nodes:
|
|
6498
|
-
* - safe read-only planner/scout/reviewer/verifier/supervisor/closeout
|
|
6499
|
-
* - exclusive implementers get a single clean-timeout transport retry
|
|
6500
|
-
* (provider stall with zero attributed writes only)
|
|
6501
|
-
* Dynamic, shell, static, and decision-gate nodes are skipped. Idempotent:
|
|
6502
|
-
* never overwrites an explicit retryPolicy a task already declares.
|
|
6503
|
-
*/
|
|
6504
6580
|
/**
|
|
6505
6581
|
* Backend-implementation Pi extension buckets (plan 2026-08-21 D3):
|
|
6506
6582
|
* - read side: every non-writer, non-closeout Pi node gets navigation
|
|
@@ -6523,6 +6599,22 @@ function applyBackendPiExtensionBuckets(spec) {
|
|
|
6523
6599
|
task.toolProfile === "write" ? ["pi-codegraph"] : ["pi-codegraph", "pi-lens"];
|
|
6524
6600
|
}
|
|
6525
6601
|
}
|
|
6602
|
+
function stampTargetTemplateTransientRetryProfile(spec) {
|
|
6603
|
+
for (const task of spec.tasks) {
|
|
6604
|
+
if (isTargetTemplateImplementPi(task) ||
|
|
6605
|
+
isCanonicalFinalVerifyShellRetryCandidate(task)) {
|
|
6606
|
+
task.transientRetryProfile = TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE;
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
/**
|
|
6611
|
+
* Apply default Pi retry policies to generated DAG nodes:
|
|
6612
|
+
* - safe read-only planner/scout/reviewer/verifier/supervisor/closeout
|
|
6613
|
+
* - exclusive implementers get a single clean-timeout transport retry
|
|
6614
|
+
* (provider stall with zero attributed writes only)
|
|
6615
|
+
* Dynamic, shell, static, and decision-gate nodes are skipped. Idempotent:
|
|
6616
|
+
* never overwrites an explicit retryPolicy a task already declares.
|
|
6617
|
+
*/
|
|
6526
6618
|
function applyDefaultReadOnlyRetryPolicy(spec) {
|
|
6527
6619
|
for (const task of spec.tasks) {
|
|
6528
6620
|
if (task.retryPolicy !== undefined)
|
|
@@ -6535,6 +6627,10 @@ function applyDefaultReadOnlyRetryPolicy(spec) {
|
|
|
6535
6627
|
: DEFAULT_READ_ONLY_PI_RETRY_POLICY;
|
|
6536
6628
|
continue;
|
|
6537
6629
|
}
|
|
6630
|
+
if (isTargetTemplateImplementPi(task)) {
|
|
6631
|
+
task.retryPolicy = TARGET_TEMPLATE_WRITER_TRANSPORT_RETRY_POLICY;
|
|
6632
|
+
continue;
|
|
6633
|
+
}
|
|
6538
6634
|
if (isWriterTransportRetryCandidate(task)) {
|
|
6539
6635
|
task.retryPolicy = WRITER_TRANSPORT_RETRY_POLICY;
|
|
6540
6636
|
}
|
|
@@ -6795,6 +6891,7 @@ function buildReviewGatedHybridDag(standard, sources) {
|
|
|
6795
6891
|
}));
|
|
6796
6892
|
spec.tasks.splice(spec.tasks.length - 1, 0, buildReviewNode(sources), buildReviewVerdictRecoveryNode(sources), buildReviewGateNode(sources));
|
|
6797
6893
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
6894
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
6798
6895
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
6799
6896
|
parseDagSpec(spec);
|
|
6800
6897
|
assertValidDagSpec(spec);
|
|
@@ -7126,6 +7223,9 @@ async function buildHardVerifyNode(sources) {
|
|
|
7126
7223
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
7127
7224
|
outputContract: "Archived hard verification stdout/stderr with exit codes; no worktree writes.",
|
|
7128
7225
|
subtask_prompt: "Run hard verification after repair round.",
|
|
7226
|
+
transientRetryProfile: sources.verifyCommands && commands.length > 0
|
|
7227
|
+
? TARGET_TEMPLATE_TRANSIENT_RETRY_PROFILE
|
|
7228
|
+
: undefined,
|
|
7129
7229
|
shell: {
|
|
7130
7230
|
commands,
|
|
7131
7231
|
verifyEvidence: buildVerifyEvidence({
|
|
@@ -7296,6 +7396,7 @@ async function buildSupervisedHybridDag(standard, sources) {
|
|
|
7296
7396
|
],
|
|
7297
7397
|
};
|
|
7298
7398
|
applySddEmbeddedEnhancements(spec, sources.sddEmbeddedSkills ?? new Set());
|
|
7399
|
+
stampTargetTemplateTransientRetryProfile(spec);
|
|
7299
7400
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
7300
7401
|
parseDagSpec(spec);
|
|
7301
7402
|
assertValidDagSpec(spec);
|
|
@@ -10,7 +10,7 @@ import { resolveContextPolicy } from "./context-policy.js";
|
|
|
10
10
|
import { buildDagNodePromptEnvelope, formatConvergenceFeedbackBlock, } from "./prompt.js";
|
|
11
11
|
import { persistLongNodeOutputArtifacts } from "./upstream-artifacts.js";
|
|
12
12
|
import { buildOutputLimitRecoverySection, loadBackendTestWriterProgressForRetry, } from "./backend-test-writer-completeness.js";
|
|
13
|
-
import { computeBackoffDelayMs, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, } from "./retry-policy.js";
|
|
13
|
+
import { computeBackoffDelayMs, isCanonicalFinalVerifyShellRetryCandidate, isRetryablePiFailureCategory, isSafeReadOnlyPiRetryCandidate, isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, VERIFY_SHELL_RETRY_POLICY, } from "./retry-policy.js";
|
|
14
14
|
import { applyNodeActivity, evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
|
|
15
15
|
import { buildProtocolRetryInstruction, normalizeReviewVerdictAfterRetries, parseJsonReviewVerdict, validateOutputProtocol, } from "./output-protocol.js";
|
|
16
16
|
import { getStructuredContractValidator } from "./contract-output-registry.js";
|
|
@@ -260,6 +260,16 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
260
260
|
"</retry_instruction>",
|
|
261
261
|
].join("\n");
|
|
262
262
|
}
|
|
263
|
+
if (previousFailureCategory === "writer-clean-timeout") {
|
|
264
|
+
return [
|
|
265
|
+
basePrompt,
|
|
266
|
+
"",
|
|
267
|
+
"<retry_instruction>",
|
|
268
|
+
"Previous attempt ended in a clean provider/transport failure with zero attributed workspace writes and zero write/edit tool calls.",
|
|
269
|
+
"Do not invent existing implementation progress. Continue from the original task only if the workspace is still unchanged.",
|
|
270
|
+
"</retry_instruction>",
|
|
271
|
+
].join("\n");
|
|
272
|
+
}
|
|
263
273
|
if (previousFailureCategory !== "output-too-large") {
|
|
264
274
|
return basePrompt;
|
|
265
275
|
}
|
|
@@ -696,12 +706,18 @@ export async function executeDagNode(input) {
|
|
|
696
706
|
// Invalid or missing harness preserves the previous no-override behavior.
|
|
697
707
|
}
|
|
698
708
|
}
|
|
699
|
-
const
|
|
709
|
+
const specRetryPolicy = task.retryPolicy &&
|
|
700
710
|
(isSafeReadOnlyPiRetryCandidate(task) ||
|
|
701
711
|
isWriterEmptyDiffRetryCandidate(task) ||
|
|
702
712
|
isWriterTransportRetryCandidate(task))
|
|
703
713
|
? task.retryPolicy
|
|
704
714
|
: undefined;
|
|
715
|
+
const implicitVerifyRetryPolicy = specRetryPolicy === undefined &&
|
|
716
|
+
isCanonicalFinalVerifyShellRetryCandidate(task) &&
|
|
717
|
+
isTargetTemplateTransientRetryNode(task)
|
|
718
|
+
? VERIFY_SHELL_RETRY_POLICY
|
|
719
|
+
: undefined;
|
|
720
|
+
const retryPolicy = specRetryPolicy ?? implicitVerifyRetryPolicy;
|
|
705
721
|
const maxAttempts = retryPolicy?.maxAttempts ?? 1;
|
|
706
722
|
const attempts = [];
|
|
707
723
|
let totalAttemptWallDurationMs = 0;
|
|
@@ -943,6 +959,7 @@ export async function executeDagNode(input) {
|
|
|
943
959
|
const attemptFinishedAt = new Date().toISOString();
|
|
944
960
|
const attemptWallDurationMs = durationBetween(attemptStartedAt, attemptFinishedAt);
|
|
945
961
|
totalAttemptWallDurationMs += attemptWallDurationMs;
|
|
962
|
+
const rawFailureCategory = result.rawFailureCategory ?? result.failureCategory;
|
|
946
963
|
const attemptRecord = {
|
|
947
964
|
attempt: attemptNumber,
|
|
948
965
|
startedAt: attemptStartedAt,
|
|
@@ -954,6 +971,7 @@ export async function executeDagNode(input) {
|
|
|
954
971
|
stderr: result.stderr,
|
|
955
972
|
assistantText: result.assistantText,
|
|
956
973
|
failureCategory: result.failureCategory,
|
|
974
|
+
...(rawFailureCategory ? { rawFailureCategory } : {}),
|
|
957
975
|
backend: result.backend,
|
|
958
976
|
sdkAttempted: result.sdkAttempted,
|
|
959
977
|
tokensUsed: result.tokensUsed,
|
|
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { assertTaskContractBindingFresh } from "./task-contract-binding.js";
|
|
4
4
|
import { isSafeReadOnlyPiRetryCandidate, isTransportCrashedExclusiveWriter } from "./retry-policy.js";
|
|
5
5
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
6
|
+
import { composeDagPromptOverridePrompt, MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS, } from "../../shared/dag-prompt-override.js";
|
|
6
7
|
const REVIEW_VERDICT_RECOVERY_NODE_ID = "review-verdict-recovery-pi";
|
|
7
8
|
const SAFE_SHELL_GATE_KEYS = [
|
|
8
9
|
"verdictGate",
|
|
@@ -381,6 +382,100 @@ export function assessResetClosureSafety(spec, resetNodeIds, options = {}) {
|
|
|
381
382
|
reasonCodes: [...new Set(reasonCodes)],
|
|
382
383
|
};
|
|
383
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* Experimental prompt-override admission (fail-closed):
|
|
387
|
+
* - requires the internal switch (see shared/dag-prompt-override.ts);
|
|
388
|
+
* - the target must be a failed (ERROR) node inside the reset closure, so an
|
|
389
|
+
* override can only adjust how an already-failed node retries — never a
|
|
390
|
+
* finished, skipped, or not-yet-started node;
|
|
391
|
+
* - only `subtask_prompt` semantics change: "append" keeps the frozen base
|
|
392
|
+
* and adds a marked addendum block, "replace" swaps the whole prompt;
|
|
393
|
+
* base/final sha256 are recorded for provenance and bound into planHash.
|
|
394
|
+
*/
|
|
395
|
+
function assessPromptOverrides(input) {
|
|
396
|
+
const blockingNodes = [];
|
|
397
|
+
const reasonCodes = [];
|
|
398
|
+
const tasks = taskById(input.spec);
|
|
399
|
+
const resetSet = new Set(input.resetNodeIds);
|
|
400
|
+
const seen = new Set();
|
|
401
|
+
const entries = [];
|
|
402
|
+
if (!input.enabled) {
|
|
403
|
+
reasonCodes.push("prompt-override-disabled");
|
|
404
|
+
}
|
|
405
|
+
for (const override of input.overrides) {
|
|
406
|
+
const nodeId = override.nodeId?.trim() ?? "";
|
|
407
|
+
if (seen.has(nodeId)) {
|
|
408
|
+
blockingNodes.push({
|
|
409
|
+
nodeId,
|
|
410
|
+
reasonCode: "prompt-override-duplicate-node",
|
|
411
|
+
});
|
|
412
|
+
reasonCodes.push("prompt-override-duplicate-node");
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
seen.add(nodeId);
|
|
416
|
+
const task = tasks.get(nodeId);
|
|
417
|
+
if (!task) {
|
|
418
|
+
blockingNodes.push({
|
|
419
|
+
nodeId,
|
|
420
|
+
reasonCode: "prompt-override-node-missing",
|
|
421
|
+
});
|
|
422
|
+
reasonCodes.push("prompt-override-node-missing");
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (!resetSet.has(nodeId)) {
|
|
426
|
+
blockingNodes.push({
|
|
427
|
+
nodeId,
|
|
428
|
+
reasonCode: "prompt-override-outside-reset-closure",
|
|
429
|
+
});
|
|
430
|
+
reasonCodes.push("prompt-override-outside-reset-closure");
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (input.state.nodes[nodeId]?.status !== "ERROR") {
|
|
434
|
+
blockingNodes.push({
|
|
435
|
+
nodeId,
|
|
436
|
+
reasonCode: "prompt-override-node-not-failed",
|
|
437
|
+
});
|
|
438
|
+
reasonCodes.push("prompt-override-node-not-failed");
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const mode = override.mode === "append" ? "append" : "replace";
|
|
442
|
+
const prompt = override.prompt ?? "";
|
|
443
|
+
if (!prompt.trim() ||
|
|
444
|
+
prompt.length > MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS) {
|
|
445
|
+
blockingNodes.push({
|
|
446
|
+
nodeId,
|
|
447
|
+
reasonCode: "prompt-override-invalid-prompt",
|
|
448
|
+
});
|
|
449
|
+
reasonCodes.push("prompt-override-invalid-prompt");
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const basePromptSha256 = createHash("sha256")
|
|
453
|
+
.update(task.subtask_prompt, "utf8")
|
|
454
|
+
.digest("hex");
|
|
455
|
+
const finalPrompt = composeDagPromptOverridePrompt({
|
|
456
|
+
basePrompt: task.subtask_prompt,
|
|
457
|
+
text: prompt,
|
|
458
|
+
mode,
|
|
459
|
+
});
|
|
460
|
+
const promptSha256 = createHash("sha256")
|
|
461
|
+
.update(finalPrompt, "utf8")
|
|
462
|
+
.digest("hex");
|
|
463
|
+
if (promptSha256 === basePromptSha256) {
|
|
464
|
+
blockingNodes.push({
|
|
465
|
+
nodeId,
|
|
466
|
+
reasonCode: "prompt-override-unchanged",
|
|
467
|
+
});
|
|
468
|
+
reasonCodes.push("prompt-override-unchanged");
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
entries.push({ nodeId, mode, basePromptSha256, promptSha256 });
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
entries: entries.length > 0 ? entries : undefined,
|
|
475
|
+
blockingNodes,
|
|
476
|
+
reasonCodes: [...new Set(reasonCodes)],
|
|
477
|
+
};
|
|
478
|
+
}
|
|
384
479
|
export function computePlanHash(planWithoutHash) {
|
|
385
480
|
const canonical = JSON.stringify(sortKeysDeep(planWithoutHash));
|
|
386
481
|
return createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
@@ -648,6 +743,22 @@ export async function evaluateDagRerunPlan(input) {
|
|
|
648
743
|
blockingNodes.push({ nodeId, reasonCode: "parent-facts-invalid" });
|
|
649
744
|
}
|
|
650
745
|
}
|
|
746
|
+
let promptOverrideEntries;
|
|
747
|
+
if (input.promptOverrides !== undefined && input.promptOverrides.length > 0) {
|
|
748
|
+
const overrideAssessment = assessPromptOverrides({
|
|
749
|
+
spec: input.parentSpec,
|
|
750
|
+
state: input.parentState,
|
|
751
|
+
resetNodeIds,
|
|
752
|
+
overrides: input.promptOverrides,
|
|
753
|
+
enabled: input.promptOverrideEnabled === true,
|
|
754
|
+
});
|
|
755
|
+
blockingNodes.push(...overrideAssessment.blockingNodes);
|
|
756
|
+
for (const code of overrideAssessment.reasonCodes) {
|
|
757
|
+
reasonCodes.push(code);
|
|
758
|
+
blockedReasons.push(code);
|
|
759
|
+
}
|
|
760
|
+
promptOverrideEntries = overrideAssessment.entries;
|
|
761
|
+
}
|
|
651
762
|
const uniqueReasonCodes = [...new Set(reasonCodes)];
|
|
652
763
|
const uniqueBlockedReasons = [...new Set(blockedReasons)];
|
|
653
764
|
const eligible = uniqueBlockedReasons.length === 0 &&
|
|
@@ -691,6 +802,7 @@ export async function evaluateDagRerunPlan(input) {
|
|
|
691
802
|
parentLifecycle: input.parentLifecycle,
|
|
692
803
|
workerManaged: input.workerManaged,
|
|
693
804
|
}),
|
|
805
|
+
...(promptOverrideEntries ? { promptOverrides: promptOverrideEntries } : {}),
|
|
694
806
|
};
|
|
695
807
|
return {
|
|
696
808
|
...planWithoutHash,
|