@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.
Files changed (140) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +153 -96
  3. package/README.md +9 -5
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +16 -5
  6. package/dist/cli/program.js +16 -2
  7. package/dist/cli/update/notifier.js +2 -2
  8. package/dist/cli.js +2 -1
  9. package/dist/commands/dag-rerun.js +55 -1
  10. package/dist/commands/init-model-catalog.js +464 -0
  11. package/dist/commands/init-upgrade.js +265 -97
  12. package/dist/commands/init.js +428 -27
  13. package/dist/commands/inspect-next.js +8 -0
  14. package/dist/executors/dag-pi-executor.js +18 -9
  15. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  16. package/dist/executors/shell-executor.js +209 -13
  17. package/dist/shared/dag-prompt-override.js +28 -0
  18. package/dist/shared/operator/capabilities.js +32 -0
  19. package/dist/shared/playwright-cli-command-policy.js +15 -0
  20. package/dist/shared/update/console-notifier.js +100 -0
  21. package/dist/{cli → shared}/update/npm-client.js +41 -8
  22. package/dist/{cli → shared}/update/state.js +41 -13
  23. package/dist/task/source-prepare/parse-intent.js +24 -4
  24. package/dist/worker/cli.js +31 -1
  25. package/dist/worker/console/chat/routes.js +1 -0
  26. package/dist/worker/console/dag-execution-receipt.js +33 -0
  27. package/dist/worker/console/operator-actions.js +116 -0
  28. package/dist/worker/console/operator-user-error.js +2 -2
  29. package/dist/worker/console/routes.js +43 -2
  30. package/dist/worker/console/security.js +63 -9
  31. package/dist/worker/console/server.js +36 -5
  32. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
  33. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-pm3c2Rsl.js} +1 -1
  34. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
  35. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
  36. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
  37. package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
  38. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
  48. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
  49. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
  50. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
  51. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
  52. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
  53. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
  56. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
  57. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
  58. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
  59. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
  60. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
  61. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
  62. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
  63. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
  64. package/dist/worker/console/static/assets/{index-Ya5FE7cD.css → index-C8rOIAJr.css} +1 -1
  65. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-DEbyrEfW.js} +71 -71
  66. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
  67. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
  68. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
  69. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
  70. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-CAyXjQjN.js} +1 -1
  71. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-CcWKn6tS.js} +5 -5
  72. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
  73. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
  74. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
  75. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
  76. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
  77. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
  78. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
  79. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
  80. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
  81. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
  82. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
  83. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
  84. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
  85. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
  86. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
  87. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
  88. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
  89. package/dist/worker/console/static/index.html +2 -2
  90. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  91. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  92. package/dist/worker/observe/node-input.js +2 -0
  93. package/dist/worker/observe/node-process.js +377 -0
  94. package/dist/worker/observe/routes.js +36 -3
  95. package/dist/worker/observe/shell-handler-keys.js +34 -0
  96. package/dist/worker/observe/static/api.js +66 -0
  97. package/dist/worker/observe/static/constants.js +8 -1
  98. package/dist/worker/observe/static/format.js +23 -0
  99. package/dist/worker/observe/static/markdown-render.js +20 -1
  100. package/dist/worker/observe/static/operator-chrome.js +4 -0
  101. package/dist/worker/observe/static/state.js +11 -0
  102. package/dist/worker/observe/static/styles.css +73 -0
  103. package/dist/worker/observe/static/views/dag-inspector.js +561 -8
  104. package/dist/worker/observe/static/views/dag.js +8 -1
  105. package/dist/worker/observe/static/views/failures.js +5 -2
  106. package/dist/worker/observe/static/views/session-timeline.js +60 -4
  107. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  108. package/dist/workflows/dag/failure-category.js +8 -0
  109. package/dist/workflows/dag/frontend-implementation-contract.js +5 -1
  110. package/dist/workflows/dag/frontend-repair.js +14 -0
  111. package/dist/workflows/dag/frontend-risk.js +15 -2
  112. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  113. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  114. package/dist/workflows/dag/frontend-test-result-contract.js +151 -11
  115. package/dist/workflows/dag/frontend-verification-trace.js +29 -4
  116. package/dist/workflows/dag/init-hybrid.js +125 -24
  117. package/dist/workflows/dag/node-execution.js +20 -2
  118. package/dist/workflows/dag/rerun-plan.js +112 -0
  119. package/dist/workflows/dag/rerun-run.js +58 -6
  120. package/dist/workflows/dag/retry-policy.js +91 -0
  121. package/dist/workflows/dag/types.js +9 -1
  122. package/docs/init-surface.manifest.json +5 -0
  123. package/docs/operations/README.md +1 -1
  124. package/docs/templates/init-managed-agents.md +13 -3
  125. package/harness.json +2 -2
  126. package/package.json +5 -1
  127. package/scripts/next-info.mjs +356 -0
  128. package/scripts/next-publish-gate.mjs +238 -0
  129. package/scripts/release-source-binding.mjs +251 -0
  130. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  131. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  132. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  133. package/skills/loop-agent/references/command-reference.md +11 -4
  134. package/skills/playwright-cli/SKILL.md +22 -0
  135. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  136. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  137. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  138. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  139. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  140. 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
- * package.json, so those fixtures retain the historical generic fallback.
1288
- * Real projects never inherit loop-agent's commands when package.json exists.
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
- return genericFallback;
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
- function extractExplicitRequirementIds(...markdownInputs) {
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
- for (const markdown of markdownInputs) {
1404
+ const collect = (markdown, declarationsOnly) => {
1389
1405
  if (!markdown)
1390
- continue;
1391
- for (const match of markdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
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 = extractExplicitRequirementIds(sectionMatch[1]);
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
- const staticFallbackCommands = fallbackVerifyCommands.staticCommands;
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
- parseDagSpec(spec);
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 retryPolicy = task.retryPolicy &&
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,