@tea-agent/loop-agent 0.38.1 → 0.38.3

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 (180) hide show
  1. package/CHANGELOG.md +126 -12
  2. package/README.md +2 -0
  3. package/bin/loop-agent.js +7 -3
  4. package/dist/application/dag/generate-task-dag.js +9 -0
  5. package/dist/application/task-lifecycle/advance.js +4 -0
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/commands/init.js +2 -14
  8. package/dist/commands/task-advance.js +31 -0
  9. package/dist/executors/dag-pi-executor.js +99 -5
  10. package/dist/executors/pi-event-serializer.js +42 -1
  11. package/dist/executors/pi-executor.js +51 -0
  12. package/dist/executors/pi-extension-resolver.js +233 -0
  13. package/dist/executors/pi-sdk-executor.js +187 -59
  14. package/dist/executors/shell-executor.js +324 -16
  15. package/dist/executors/shell-write-guard.js +7 -0
  16. package/dist/governance/manifest-types.js +0 -2
  17. package/dist/shared/operator/capabilities.js +87 -22
  18. package/dist/task/config-types.js +22 -0
  19. package/dist/task/contract/constants.js +1 -0
  20. package/dist/task/contract/diff.js +26 -0
  21. package/dist/task/contract/project.js +5 -0
  22. package/dist/task/contract/schema.js +6 -1
  23. package/dist/task/source-prepare/build-draft.js +15 -0
  24. package/dist/worker/console/chat/deferred-turn.js +91 -0
  25. package/dist/worker/console/chat/pi-runtime.js +10 -5
  26. package/dist/worker/console/chat/routes.js +458 -47
  27. package/dist/worker/console/chat/semantic-activity.js +13 -13
  28. package/dist/worker/console/chat/session-store.js +146 -2
  29. package/dist/worker/console/harness-pi-model-matrix.js +2 -11
  30. package/dist/worker/console/operator-actions.js +5 -4
  31. package/dist/worker/console/pi-readiness.js +5 -4
  32. package/dist/worker/console/recovery-cta.js +4 -4
  33. package/dist/worker/console/recovery-error-copy.js +4 -4
  34. package/dist/worker/console/server.js +6 -1
  35. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Hfsot6rP.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
  36. package/dist/worker/console/static/assets/{arc-YGwEASin.js → arc-Bj2M8iO5.js} +1 -1
  37. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CmOM0F92.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
  38. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DDLuaHC5.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
  39. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B0Um43W4.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
  40. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
  41. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DvBN4JZc.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B1UVgW4E.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Cf0GgXzX.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DbNA24sj.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bi_0Be06.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C8pPGvRd.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BitQY8Z-.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
  48. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-U67GCiEo.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
  49. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-IoJwXPBA.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
  50. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-YDZ_UHgE.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
  51. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
  52. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
  53. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CUZgsiRC.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
  54. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-COmCh4pT.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
  55. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Ta6FSm68.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
  56. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UdcR-VW9.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
  57. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DybcO3Et.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
  58. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-wErDFWLw.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
  59. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DkVHZWqz.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
  60. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DdqWs7QN.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
  61. package/dist/worker/console/static/assets/{diagram-UB23O5K3-C9dMVmDo.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
  62. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B2T2WcBK.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
  63. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt9rjDFq.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
  64. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-U8ic_tvi.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
  65. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BfQJdQoO.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
  66. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CpMftH4O.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
  67. package/dist/worker/console/static/assets/index-DTZOKgAn.js +404 -0
  68. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
  69. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DzqWXNO_.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
  70. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
  71. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ChPtKFLz.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
  72. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PRe7wB5u.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
  73. package/dist/worker/console/static/assets/{linear-DpKX6QNp.js → linear-DdnapKIH.js} +1 -1
  74. package/dist/worker/console/static/assets/{mermaid.core-Dnl76b7Y.js → mermaid.core-BVjAT9b8.js} +5 -5
  75. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-S9PCXc3J.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
  76. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Du8e-FSX.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
  77. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CY6OmHTc.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
  78. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C65GPhpj.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
  79. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-U6IUiTCL.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
  80. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-I_9Qhi.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
  81. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B57Fhed0.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
  82. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CJOtiw21.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
  83. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-FyX8zxSh.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
  84. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eF-pbs9t.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
  85. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
  86. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CcfkA-Dq.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
  87. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
  88. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DH8FjnOK.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
  89. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-beoTagJg.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
  90. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CQSfw4cK.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
  91. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQgzQ6jn.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
  92. package/dist/worker/console/static/index.html +2 -2
  93. package/dist/worker/console/static-src/app/console-types.js +0 -12
  94. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  95. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  96. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +38 -1
  97. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  98. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  99. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  100. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  101. package/dist/worker/console/static-src/operator-chat/useChatStream.js +204 -11
  102. package/dist/worker/console/static-src/operator-chat/useChatThread.js +23 -1
  103. package/dist/worker/observe/node-input.js +72 -3
  104. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  105. package/dist/worker/observe/static/state.js +2 -2
  106. package/dist/worker/observe/static/views/dag-inspector.js +51 -0
  107. package/dist/worker/observe/static/views/session-timeline.js +18 -5
  108. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +809 -53
  109. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  110. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  111. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  112. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  113. package/dist/workflows/dag/backend-test-layout.js +133 -0
  114. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  115. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  116. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  117. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  118. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  119. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  120. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  121. package/dist/workflows/dag/contract-output-registry.js +15 -0
  122. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  123. package/dist/workflows/dag/convergence/controller.js +141 -0
  124. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  125. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  126. package/dist/workflows/dag/failure-category.js +4 -0
  127. package/dist/workflows/dag/frontend-implementation-contract.js +355 -26
  128. package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
  129. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  130. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  131. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  132. package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
  133. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  134. package/dist/workflows/dag/init-hybrid.js +720 -177
  135. package/dist/workflows/dag/node-execution.js +164 -26
  136. package/dist/workflows/dag/prompt.js +60 -1
  137. package/dist/workflows/dag/rerun-feedback.js +212 -0
  138. package/dist/workflows/dag/rerun-plan.js +112 -27
  139. package/dist/workflows/dag/rerun-run.js +3 -0
  140. package/dist/workflows/dag/rerun-task.js +61 -2
  141. package/dist/workflows/dag/retry-policy.js +18 -0
  142. package/dist/workflows/dag/structured-output-repair.js +712 -0
  143. package/dist/workflows/dag/types.js +121 -0
  144. package/dist/workflows/dag/validate.js +41 -1
  145. package/docs/operations/local-development-environment.md +1 -5
  146. package/docs/skills/vetted-skill-registry.md +14 -0
  147. package/docs/templates/README.md +1 -1
  148. package/docs/templates/agent-dag.schema.json +44 -0
  149. package/docs/templates/backend-test-dag.json +60 -35
  150. package/docs/templates/frontend-test-dag.json +8 -10
  151. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  152. package/docs/templates/harness.schema.json +0 -8
  153. package/harness.json +1 -8
  154. package/package.json +8 -4
  155. package/skills/analyze-product-requirements/SKILL.md +8 -5
  156. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  157. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  158. package/skills/analyze-product-requirements/references/example.md +50 -0
  159. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  160. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  161. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  162. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  163. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  164. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  165. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  166. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  167. package/skills/codebase-scout/SKILL.md +1 -1
  168. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  169. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  170. package/skills/improve-codebase-architecture/deepening.md +37 -0
  171. package/skills/improve-codebase-architecture/html-report.md +123 -0
  172. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  173. package/skills/improve-codebase-architecture/language.md +53 -0
  174. package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +0 -1
  175. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +0 -1
  176. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +0 -1
  177. package/dist/worker/console/static/assets/index-BA5-N5ER.css +0 -1
  178. package/dist/worker/console/static/assets/index-DKPXBrdc.js +0 -325
  179. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +0 -1
  180. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +0 -8
@@ -15,6 +15,8 @@ import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend
15
15
  import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
16
16
  import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
17
17
  import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
18
+ import { copyFrontendTestStandardScenarios } from "../workflows/dag/frontend-test-standard-scenarios.js";
19
+ import { probeFrontendTestEnvironment } from "../workflows/dag/frontend-test-environment-probe.js";
18
20
  import { renderFrontendTestL5Report } from "../workflows/dag/frontend-test-l5-report.js";
19
21
  import { validateFrontendCaseChecklist } from "../workflows/dag/frontend-test-case-checklist.js";
20
22
  import { materializeFrontendTestCaseManifest } from "../workflows/dag/frontend-test-case-manifest.js";
@@ -27,11 +29,13 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
27
29
  import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
28
30
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
29
31
  import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
32
+ import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
33
+ import { backendTestGapDocSchema, ingestBackendTestGap, } from "../workflows/dag/backend-test-gap-fill.js";
30
34
  import { analyzeBackendTestCaseCoverage, analyzeBackendTestMarkdownPytestCorrespondence, materializeBackendTestCaseManifestFromFacts, } from "../workflows/dag/backend-test-case-coverage-analysis.js";
31
35
  import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport, } from "../workflows/dag/backend-test-result-contract.js";
32
- import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
36
+ import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, resolveBackendTestMappedPytestScripts, collectJacocoCoverage, hasBlockingBackendMarkdownSafetyFindings, hasBlockingBackendMarkdownModuleStemFindings, inspectBackendTestEnvironment, markdownFiles, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, renderBackendTestL5Dashboard, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
33
37
  import { applyDeterministicScenarioParamRepairs, assessBackendScenarioParamConsistency, classifyBackendTestFailureWithScenarioParam, readBackendScenarioParamFacts, renderBackendTestFailureAnalysis, writeBackendScenarioParamArtifacts, writeScenarioParamRepairAudit, } from "../workflows/dag/backend-test-scenario-param.js";
34
- import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
38
+ import { assessBackendPytestCollection, assessMissingBackendPytestScripts, assessPriorityOnlyBackendPytestModules, assertBackendTestExecutionReadinessFresh, buildBackendPytestAssetInventory, buildBackendTestItemEligibility, materializeBackendTestExecutionReadiness, materializeEffectiveBackendPytestCollection, readBackendPytestCollectionFacts, readBackendTestExecutionReadiness, writeBackendPytestCollectionArtifacts, } from "../workflows/dag/backend-test-pytest-collection.js";
35
39
  import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
36
40
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
37
41
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
@@ -48,6 +52,7 @@ async function runBackendScenarioParamPreflight(input) {
48
52
  phase: "initial",
49
53
  repairAttempt: 0,
50
54
  strictScenarioParamGate: input.strictScenarioParamGate,
55
+ layout: input.layout,
51
56
  });
52
57
  const initialArtifacts = await writeBackendScenarioParamArtifacts({
53
58
  runDir: input.runDir,
@@ -73,6 +78,7 @@ async function runBackendScenarioParamPreflight(input) {
73
78
  phase: "final",
74
79
  repairAttempt: 1,
75
80
  strictScenarioParamGate: input.strictScenarioParamGate,
81
+ layout: input.layout,
76
82
  });
77
83
  finalFacts = reassessed.facts;
78
84
  finalMarkdown = reassessed.markdown;
@@ -492,11 +498,114 @@ async function readAdvisoryRunReport(reportsDir, filename, title) {
492
498
  async function executeBackendTestPipeline(input, meta) {
493
499
  const pipeline = input.task.shell?.backendTestPipeline;
494
500
  const started = Date.now();
501
+ // Plan A: prefer the frozen layout carried by the compiled spec; fall back
502
+ // to the historical default for hand-written legacy DAGs.
503
+ const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
504
+ ? {
505
+ testRoot: meta.spec.backendTestLayout.testRoot,
506
+ markdownRel: meta.spec.backendTestLayout.markdownRel,
507
+ scriptRel: meta.spec.backendTestLayout.scriptRel,
508
+ }
509
+ : undefined);
495
510
  try {
496
511
  if (!pipeline)
497
512
  throw new Error("missing backend-test pipeline id");
498
513
  const outputs = [];
499
- if (pipeline === "markdown-environment") {
514
+ if (pipeline === "ingest-backend-test-gap") {
515
+ // Plan D: deterministic gap ingest. Identity (taskId + requirement
516
+ // sha256) is fail-closed; a mismatched plan blocks every writer.
517
+ const gapDocRel = meta.spec.globalConstraints
518
+ ?.map((item) => /^backendTestGapDoc=(.+)$/i.exec(item.trim())?.[1])
519
+ .find(Boolean);
520
+ const binding = meta.spec.sourceBinding;
521
+ const requirementSha256 = binding?.sources.find((s) => s.kind === "requirement")?.sha256 ?? "";
522
+ if (!gapDocRel || !binding || !requirementSha256) {
523
+ return {
524
+ ok: false,
525
+ stdout: outputs.join("\n"),
526
+ stderr: "backend-test gap ingest requires the bound gap document path and the task source binding with a requirement sha256",
527
+ failureCategory: "invalid-output",
528
+ durationMs: Date.now() - started,
529
+ };
530
+ }
531
+ const gapDocAbs = path.join(input.cwd, ...gapDocRel.split("/"));
532
+ let parsed;
533
+ try {
534
+ parsed = JSON.parse(await readFile(gapDocAbs, "utf8"));
535
+ }
536
+ catch (error) {
537
+ return {
538
+ ok: false,
539
+ stdout: outputs.join("\n"),
540
+ stderr: `backend-test gap document is unreadable JSON at ${gapDocRel}: ${error instanceof Error ? error.message : String(error)}`,
541
+ failureCategory: "invalid-output",
542
+ durationMs: Date.now() - started,
543
+ };
544
+ }
545
+ const doc = backendTestGapDocSchema.safeParse(parsed);
546
+ if (!doc.success) {
547
+ return {
548
+ ok: false,
549
+ stdout: outputs.join("\n"),
550
+ stderr: `invalid backend-test-gap-v1 document: ${doc.error.issues
551
+ .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
552
+ .join("; ")}`,
553
+ failureCategory: "invalid-output",
554
+ durationMs: Date.now() - started,
555
+ };
556
+ }
557
+ // previous N6 facts (may be absent on a fresh checkout)
558
+ const factsPath = path.join(input.cwd, "contracts", "backend-test-case-coverage-facts.json");
559
+ let previousMissingSlots;
560
+ let previousCoveredSlots;
561
+ try {
562
+ const facts = JSON.parse(await readFile(factsPath, "utf8"));
563
+ previousMissingSlots = facts.scenarioPartitions?.missingSlots;
564
+ previousCoveredSlots = (facts.rules ?? []).flatMap((rule) => rule.coveredTestPoints ?? []);
565
+ }
566
+ catch {
567
+ // no previous facts in the workspace: the gap doc alone drives the plan
568
+ }
569
+ // module index comes from the README on disk
570
+ let existingModuleStems = [];
571
+ try {
572
+ const files = await markdownFiles(input.cwd, backendLayout);
573
+ existingModuleStems = files
574
+ .map((file) => path.basename(file, ".md"))
575
+ .filter((stem) => stem.toLowerCase() !== "readme");
576
+ }
577
+ catch {
578
+ // no markdown tree: every slot becomes a conflict
579
+ }
580
+ const plan = ingestBackendTestGap({
581
+ gapDoc: doc.data,
582
+ previousMissingSlots,
583
+ previousCoveredSlots,
584
+ existingModuleStems,
585
+ sharedSetupPath: meta.spec.backendTestSharedSetup?.path ?? null,
586
+ layout: backendLayout,
587
+ taskId: binding.taskId,
588
+ requirementSha256,
589
+ });
590
+ const contractsDir = path.join(meta.runDir, "contracts");
591
+ await mkdir(contractsDir, { recursive: true });
592
+ const planPath = path.join(contractsDir, "backend-test-gap-plan-v1.json");
593
+ await writeFile(planPath, JSON.stringify(plan, null, 2), "utf8");
594
+ outputs.push(`gapPlan=${planPath}`, `gapPlanStatus=${plan.status}`);
595
+ if (plan.status === "error") {
596
+ return {
597
+ ok: false,
598
+ stdout: outputs.join("\n"),
599
+ stderr: [
600
+ "backend-test gap ingest failed closed (identity/conflicts):",
601
+ ...plan.conflicts,
602
+ ].join("\n"),
603
+ failureCategory: "invalid-output",
604
+ durationMs: Date.now() - started,
605
+ };
606
+ }
607
+ }
608
+ else if (pipeline === "markdown-environment") {
500
609
  const results = await executePipelineCommands(input, meta);
501
610
  if (!results.every((result) => result.ok)) {
502
611
  const failure = results.find((result) => !result.ok);
@@ -530,6 +639,8 @@ async function executeBackendTestPipeline(input, meta) {
530
639
  environmentMarkdown,
531
640
  requiredRequirementIds,
532
641
  sourceBinding: meta.spec.sourceBinding,
642
+ layout: backendLayout,
643
+ sharedSetup: meta.spec.backendTestSharedSetup ?? null,
533
644
  });
534
645
  }
535
646
  catch (error) {
@@ -583,6 +694,7 @@ async function executeBackendTestPipeline(input, meta) {
583
694
  const coverage = await analyzeBackendTestCaseCoverage({
584
695
  workspaceRoot: input.cwd,
585
696
  sourceBinding: normalizedBinding,
697
+ layout: backendLayout,
586
698
  });
587
699
  const coveragePath = await writeRunReport(meta.runDir, "backend-test-case-coverage-analysis.md", coverage.markdown);
588
700
  const contractsDir = path.join(meta.runDir, "contracts");
@@ -590,6 +702,23 @@ async function executeBackendTestPipeline(input, meta) {
590
702
  const factsPath = path.join(contractsDir, "backend-test-case-coverage-facts.json");
591
703
  await writeFile(factsPath, JSON.stringify(coverage.facts, null, 2), "utf8");
592
704
  outputs.push(`caseCoverage=${coveragePath}`, `caseCoverageFacts=${factsPath}`, coverage.markdown);
705
+ // Plan C: scenario-partition slot coverage is fail-closed. General
706
+ // coverage findings stay advisory, but a declared filter axis missing
707
+ // an each-value/omitted slot or its complement slot must block pytest
708
+ // generation until the Markdown declares the slot.
709
+ const partitionBlocking = coverage.facts.findings.filter((finding) => /^(?:MISSING_PARTITION|MISSING_PARTITION_SLOT|MISSING_COMPLEMENT_SLOT|MISSING_PARTITION_TABLE|INVALID_PARTITION_DOMAIN|NO_FINITE_DOMAIN|PARTITION_SOURCE_MISMATCH)/.test(finding));
710
+ if (partitionBlocking.length > 0) {
711
+ return {
712
+ ok: false,
713
+ stdout: outputs.join("\n\n"),
714
+ stderr: [
715
+ "backend-test scenario partitions are incomplete (fail-closed):",
716
+ ...partitionBlocking,
717
+ ].join("\n"),
718
+ failureCategory: "invalid-output",
719
+ durationMs: Date.now() - started,
720
+ };
721
+ }
593
722
  }
594
723
  catch (error) {
595
724
  const message = error instanceof Error ? error.message : String(error);
@@ -606,9 +735,9 @@ async function executeBackendTestPipeline(input, meta) {
606
735
  }
607
736
  }
608
737
  else if (pipeline === "markdown-collection-assess") {
609
- const resolution = await resolveBackendTestMappedPytestScripts(input.cwd);
738
+ const resolution = await resolveBackendTestMappedPytestScripts(input.cwd, backendLayout);
610
739
  let facts;
611
- const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
740
+ const preflightInventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
612
741
  const priorityOnlyFacts = assessPriorityOnlyBackendPytestModules({
613
742
  phase: "initial",
614
743
  mappedScripts: resolution.expectedScripts,
@@ -618,7 +747,7 @@ async function executeBackendTestPipeline(input, meta) {
618
747
  facts = priorityOnlyFacts;
619
748
  }
620
749
  else if (resolution.missingScripts.length > 0) {
621
- const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false });
750
+ const inventory = await buildBackendPytestAssetInventory(input.cwd, resolution.existingScripts, { requireMappedScripts: false, layout: backendLayout });
622
751
  facts = assessMissingBackendPytestScripts({
623
752
  mappedScripts: resolution.expectedScripts,
624
753
  existingMappedScripts: resolution.existingScripts,
@@ -632,10 +761,11 @@ async function executeBackendTestPipeline(input, meta) {
632
761
  const scenario = await runBackendScenarioParamPreflight({
633
762
  workspaceRoot: input.cwd,
634
763
  runDir: meta.runDir,
764
+ layout: backendLayout,
635
765
  strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
636
766
  });
637
767
  outputs.push(...scenario.outputs);
638
- const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
768
+ const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
639
769
  const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
640
770
  const targets = mappedScripts.map(shellQuote).join(" ");
641
771
  const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
@@ -669,6 +799,7 @@ async function executeBackendTestPipeline(input, meta) {
669
799
  fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
670
800
  }
671
801
  facts = assessBackendPytestCollection({
802
+ layout: backendLayout,
672
803
  phase: "initial",
673
804
  mappedScripts,
674
805
  inventory,
@@ -678,6 +809,43 @@ async function executeBackendTestPipeline(input, meta) {
678
809
  fixtureResolution,
679
810
  });
680
811
  }
812
+ try {
813
+ const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
814
+ workspaceRoot: input.cwd,
815
+ layout: backendLayout,
816
+ taskId: meta.spec.sourceBinding?.taskId ?? "unbound",
817
+ });
818
+ const correspondenceReportPath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence-initial.md", correspondence.markdown);
819
+ const contractsDir = path.join(meta.runDir, "contracts");
820
+ await mkdir(contractsDir, { recursive: true });
821
+ const correspondenceFactsPath = path.join(contractsDir, "backend-test-markdown-pytest-correspondence-initial.json");
822
+ await writeFile(correspondenceFactsPath, JSON.stringify(correspondence.facts, null, 2), "utf8");
823
+ outputs.push(`initialCorrespondence=${correspondenceReportPath}`, `initialCorrespondenceFacts=${correspondenceFactsPath}`);
824
+ if (correspondence.facts.status === "FAIL" && facts.status === "PASS") {
825
+ const correspondenceRepairPaths = Array.from(new Set(correspondence.facts.entries
826
+ .filter((entry) => entry.status !== "EXACT_1_TO_1")
827
+ .flatMap((entry) => [entry.expectedScript, ...entry.actualScripts])
828
+ .filter((candidate) => candidate.startsWith("testcase/") && candidate.endsWith(".py")))).sort();
829
+ if (correspondenceRepairPaths.length > 0) {
830
+ facts.status = "REPAIRABLE";
831
+ facts.repairEligible = true;
832
+ facts.repairPaths = Array.from(new Set([...facts.repairPaths, ...correspondenceRepairPaths])).sort();
833
+ facts.findings.push({
834
+ kind: "markdown-pytest-correspondence",
835
+ classification: "test-asset-defect",
836
+ repairability: "repairable",
837
+ detail: correspondence.facts.entries
838
+ .filter((entry) => entry.status !== "EXACT_1_TO_1")
839
+ .flatMap((entry) => entry.findings)
840
+ .join("; ")
841
+ .slice(0, 12_000) || "Markdown-to-pytest correspondence is incomplete",
842
+ });
843
+ }
844
+ }
845
+ }
846
+ catch (error) {
847
+ outputs.push(`initialCorrespondence=UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`);
848
+ }
681
849
  const artifacts = await writeBackendPytestCollectionArtifacts({
682
850
  runDir: meta.runDir,
683
851
  stem: "initial",
@@ -711,14 +879,15 @@ async function executeBackendTestPipeline(input, meta) {
711
879
  });
712
880
  }
713
881
  else if (initial.status === "REPAIRABLE") {
714
- const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
882
+ const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
715
883
  const scenario = await runBackendScenarioParamPreflight({
716
884
  workspaceRoot: input.cwd,
717
885
  runDir: meta.runDir,
886
+ layout: backendLayout,
718
887
  strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
719
888
  });
720
889
  outputs.push(...scenario.outputs);
721
- const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts);
890
+ const inventory = await buildBackendPytestAssetInventory(input.cwd, mappedScripts, { layout: backendLayout });
722
891
  const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
723
892
  const targets = mappedScripts.map(shellQuote).join(" ");
724
893
  const command = `PYTHONDONTWRITEBYTECODE=1 python -m pytest --collect-only -q -p no:cacheprovider ${targets}`;
@@ -753,6 +922,7 @@ async function executeBackendTestPipeline(input, meta) {
753
922
  fixtureResolution = { exitCode: fixtureResult.exitCode ?? 2, stdout: fixtureResult.stdout, stderr: fixtureResult.stderr };
754
923
  }
755
924
  const finalFacts = assessBackendPytestCollection({
925
+ layout: backendLayout,
756
926
  phase: "final",
757
927
  mappedScripts,
758
928
  inventory,
@@ -776,12 +946,57 @@ async function executeBackendTestPipeline(input, meta) {
776
946
  facts: effective,
777
947
  });
778
948
  const scenarioFacts = await readBackendScenarioParamFacts(path.join(meta.runDir, "contracts", "backend-test-scenario-param-consistency-facts.json"));
949
+ const correspondence = meta.spec.sourceBinding
950
+ ? await analyzeBackendTestMarkdownPytestCorrespondence({
951
+ workspaceRoot: input.cwd,
952
+ layout: backendLayout,
953
+ taskId: meta.spec.sourceBinding.taskId,
954
+ })
955
+ : undefined;
956
+ const eligibility = correspondence
957
+ ? buildBackendTestItemEligibility(effective.collectedItemIds, {
958
+ correspondenceEntries: correspondence.facts.entries,
959
+ scenarioEntries: scenarioFacts.entries,
960
+ })
961
+ : { eligibleItemIds: effective.collectedItemIds, excludedItems: [] };
962
+ const eligibilityInputHashes = Object.fromEntries((correspondence?.facts.inputFiles ?? []).map((item) => [item.path, item.sha256]));
963
+ const eligibilityFactsPath = path.join(meta.runDir, "contracts", "backend-test-execution-eligibility.json");
964
+ await mkdir(path.dirname(eligibilityFactsPath), { recursive: true });
965
+ await writeFile(eligibilityFactsPath, `${JSON.stringify({
966
+ schemaId: "backend-test-execution-eligibility-v1",
967
+ status: eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
968
+ collectedItemCount: effective.collectedItemIds.length,
969
+ eligibleItemCount: eligibility.eligibleItemIds.length,
970
+ excludedItemCount: eligibility.excludedItems.length,
971
+ eligibleItemIds: eligibility.eligibleItemIds,
972
+ excludedItems: eligibility.excludedItems,
973
+ inputHashes: eligibilityInputHashes,
974
+ }, null, 2)}\n`, "utf8");
975
+ const eligibilityReportPath = await writeRunReport(meta.runDir, "backend-test-execution-eligibility.md", [
976
+ "# Backend Test Execution Eligibility",
977
+ "",
978
+ "## Status",
979
+ "",
980
+ eligibility.eligibleItemIds.length === 0 ? "BLOCKED" : eligibility.excludedItems.length > 0 ? "PARTIAL" : "PASS",
981
+ "",
982
+ `- Collected items: ${effective.collectedItemIds.length}`,
983
+ `- Eligible items: ${eligibility.eligibleItemIds.length}`,
984
+ `- Excluded items: ${eligibility.excludedItems.length}`,
985
+ "",
986
+ "## Excluded Items",
987
+ "",
988
+ ...(eligibility.excludedItems.length > 0 ? eligibility.excludedItems.map((item) => `- \`${item.itemId}\` (${item.caseId ?? "unknown Case"}): ${item.reasons.join("; ")}`) : ["- None"]),
989
+ "",
990
+ ].join("\n"));
991
+ outputs.push(`executionEligibility=${eligibilityReportPath}`, `executionEligibilityFacts=${eligibilityFactsPath}`);
779
992
  const readiness = await materializeBackendTestExecutionReadiness({
780
993
  runDir: meta.runDir,
781
994
  workspaceRoot: input.cwd,
782
995
  effective,
783
996
  scenarioParamStatus: scenarioFacts.overallStatus,
784
997
  scenarioParamRepairAttempt: scenarioFacts.repairAttempt,
998
+ eligibility,
999
+ eligibilityInputHashes,
785
1000
  });
786
1001
  return {
787
1002
  ok: true,
@@ -792,6 +1007,8 @@ async function executeBackendTestPipeline(input, meta) {
792
1007
  collectedItemCount: effective.collectedItemCount,
793
1008
  fixtureResolutionStatus: effective.fixtureResolutionStatus,
794
1009
  executionReadinessStatus: readiness.status,
1010
+ eligibleItemCount: readiness.eligibleItemIds.length,
1011
+ excludedItemCount: readiness.excludedItems.length,
795
1012
  factsPath: "contracts/backend-test-pytest-collection-effective.json",
796
1013
  readinessPath: "contracts/backend-test-execution-readiness.json",
797
1014
  reportPath: artifacts.reportPath,
@@ -826,6 +1043,7 @@ async function executeBackendTestPipeline(input, meta) {
826
1043
  try {
827
1044
  const correspondence = await analyzeBackendTestMarkdownPytestCorrespondence({
828
1045
  workspaceRoot: input.cwd,
1046
+ layout: backendLayout,
829
1047
  taskId: sourceBinding.taskId,
830
1048
  });
831
1049
  const correspondencePath = await writeRunReport(meta.runDir, "backend-test-markdown-pytest-correspondence.md", correspondence.markdown);
@@ -888,9 +1106,32 @@ async function executeBackendTestPipeline(input, meta) {
888
1106
  else if (pipeline === "markdown-execute-html") {
889
1107
  const executionReadiness = await readBackendTestExecutionReadiness(path.join(meta.runDir, "contracts", "backend-test-execution-readiness.json"));
890
1108
  await assertBackendTestExecutionReadinessFresh(input.cwd, executionReadiness);
891
- const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
892
- const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
893
- const pytestTargets = mappedScripts.map(shellQuote).join(" ");
1109
+ const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd, backendLayout);
1110
+ let eligibleItemIds = executionReadiness.eligibleItemIds;
1111
+ // Plan D gap-fill executeScope=affected: when a gap plan exists in
1112
+ // this run, prefer the newly planned items (pytest node ids contain
1113
+ // the TP slot id) plus every collected item of the patched modules;
1114
+ // scope=all keeps the full eligible set.
1115
+ const affectedScope = meta.spec.globalConstraints?.some((item) => /^backendTestExecuteScope=affected$/i.test(item.trim())) ?? false;
1116
+ if (affectedScope) {
1117
+ try {
1118
+ const plan = JSON.parse(await readFile(path.join(meta.runDir, "contracts", "backend-test-gap-plan-v1.json"), "utf8"));
1119
+ const newPoints = new Set(plan.newTestPoints ?? []);
1120
+ const modules = new Set((plan.targetModules ?? []).map((stem) => `test_${stem}.py`));
1121
+ const affected = eligibleItemIds.filter((id) => modules.size === 0 ||
1122
+ [...modules].some((script) => id.includes(script)) ||
1123
+ [...newPoints].some((point) => id.includes(point)));
1124
+ if (affected.length > 0)
1125
+ eligibleItemIds = affected;
1126
+ }
1127
+ catch {
1128
+ // no gap plan in this run: execute the full eligible set
1129
+ }
1130
+ }
1131
+ const pytestTargetsRelative = "reports/backend-test-pytest-targets.txt";
1132
+ const pytestTargetsAbsolute = path.join(meta.runDir, pytestTargetsRelative);
1133
+ await writeFile(pytestTargetsAbsolute, `${eligibleItemIds.join("\n")}\n`, "utf8");
1134
+ const pytestTargetsArg = `@\"\${HARNESS_DAG_RUN_DIR}/${pytestTargetsRelative}\"`;
894
1135
  // Split into short bash -c commands (aligned with markdown-environment).
895
1136
  // A single ultra-long compound command has been observed on Windows to
896
1137
  // exit 0xC0000142 (STATUS_DLL_INIT_FAILED) in ~40ms with empty stdout/
@@ -967,8 +1208,10 @@ async function executeBackendTestPipeline(input, meta) {
967
1208
  'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
968
1209
  'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
969
1210
  'echo "STEP=pytest-run"',
970
- `echo "- pytestCommand: python -m pytest ${mappedScripts.join(" ")} -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
971
- `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargets} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
1211
+ `echo "- pytestCommand: python -m pytest <${eligibleItemIds.length} eligibility-authorized-items> -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
1212
+ `echo "- eligibleItemCount: ${eligibleItemIds.length}" >> "\${DIAG_FILE}"`,
1213
+ `echo "- excludedItemCount: ${executionReadiness.excludedItems.length}" >> "\${DIAG_FILE}"`,
1214
+ `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargetsArg} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
972
1215
  "STATUS=$?",
973
1216
  'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
974
1217
  'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
@@ -1053,11 +1296,13 @@ async function executeBackendTestPipeline(input, meta) {
1053
1296
  htmlContent: pytestHtmlContent,
1054
1297
  pytestExitCode,
1055
1298
  });
1056
- const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
1299
+ const cases = await collectBackendTestHumanCaseCatalog(input.cwd, backendLayout);
1057
1300
  const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
1058
1301
  const caseCoverageSummary = await readAdvisoryRunReport(reportsDir, "backend-test-case-coverage-analysis.md", "Backend Test Case Coverage Analysis");
1059
1302
  const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
1060
- const correspondenceSummary = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1303
+ const correspondenceSummaryBase = await readAdvisoryRunReport(reportsDir, "backend-test-markdown-pytest-correspondence.md", "Backend Test Markdown → pytest Correspondence");
1304
+ const eligibilitySummary = await readAdvisoryRunReport(reportsDir, "backend-test-execution-eligibility.md", "Backend Test Execution Eligibility");
1305
+ const correspondenceSummary = `${correspondenceSummaryBase.trim()}\n\n${eligibilitySummary.trim()}\n`;
1061
1306
  const htmlContent = renderBackendTestHtml({
1062
1307
  title: meta.spec.title,
1063
1308
  parsed,
@@ -1245,6 +1490,7 @@ async function executeBackendTestPipeline(input, meta) {
1245
1490
  fromNodeId: "backend-test-execution-envelope",
1246
1491
  artifactName: "backend-test-execution.json",
1247
1492
  outputDir: "contracts",
1493
+ allowedTestRoot: backendLayout.testRoot,
1248
1494
  });
1249
1495
  outputs.push(`analysis=${analysis.path}`, `execution=${execution.path}`);
1250
1496
  }
@@ -1360,6 +1606,7 @@ async function executeBackendTestPipeline(input, meta) {
1360
1606
  artifactName: "backend-test-result-initial.json",
1361
1607
  outputDir: "contracts",
1362
1608
  junitRelativePath: "reports/backend-test-initial-junit.xml",
1609
+ testRoot: backendLayout.testRoot,
1363
1610
  });
1364
1611
  outputs.push(...results.map((result) => result.stdout), `result=${artifact.path}`);
1365
1612
  }
@@ -1816,6 +2063,52 @@ async function executeFrontendTestHtmlReport(input, meta) {
1816
2063
  };
1817
2064
  }
1818
2065
  }
2066
+ async function executeFrontendTestStandardScenarios(input) {
2067
+ const started = Date.now();
2068
+ try {
2069
+ const result = await copyFrontendTestStandardScenarios({
2070
+ workspaceRoot: input.cwd,
2071
+ });
2072
+ return {
2073
+ ok: true,
2074
+ stdout: JSON.stringify(result),
2075
+ stderr: "",
2076
+ failureCategory: "success",
2077
+ durationMs: Date.now() - started,
2078
+ };
2079
+ }
2080
+ catch (error) {
2081
+ return {
2082
+ ok: false,
2083
+ stdout: "",
2084
+ stderr: error instanceof Error ? error.message : String(error),
2085
+ failureCategory: "invalid-output",
2086
+ durationMs: Date.now() - started,
2087
+ };
2088
+ }
2089
+ }
2090
+ async function executeFrontendTestEnvironmentProbe(input) {
2091
+ const started = Date.now();
2092
+ const result = await probeFrontendTestEnvironment({
2093
+ workspaceRoot: input.cwd,
2094
+ });
2095
+ if (result.ok) {
2096
+ return {
2097
+ ok: true,
2098
+ stdout: result.stdout,
2099
+ stderr: "",
2100
+ failureCategory: "success",
2101
+ durationMs: Date.now() - started,
2102
+ };
2103
+ }
2104
+ return {
2105
+ ok: false,
2106
+ stdout: "",
2107
+ stderr: result.stderr,
2108
+ failureCategory: "nonzero-exit",
2109
+ durationMs: Date.now() - started,
2110
+ };
2111
+ }
1819
2112
  async function executeFrontendTestEvidenceValidation(input) {
1820
2113
  const started = Date.now();
1821
2114
  try {
@@ -2094,6 +2387,12 @@ export async function executeDagShellNode(input, meta) {
2094
2387
  };
2095
2388
  }
2096
2389
  }
2390
+ if (shell?.frontendTestStandardScenarios) {
2391
+ return executeFrontendTestStandardScenarios(input);
2392
+ }
2393
+ if (shell?.frontendTestEnvironmentProbe) {
2394
+ return executeFrontendTestEnvironmentProbe(input);
2395
+ }
2097
2396
  if (shell?.frontendBrowserToolPreflight) {
2098
2397
  const started = Date.now();
2099
2398
  try {
@@ -2246,6 +2545,13 @@ export async function executeDagShellNode(input, meta) {
2246
2545
  }
2247
2546
  if (shell?.jsonArtifactGate) {
2248
2547
  const started = Date.now();
2548
+ const backendLayout = resolveBackendTestLayout(meta.spec.backendTestLayout
2549
+ ? {
2550
+ testRoot: meta.spec.backendTestLayout.testRoot,
2551
+ markdownRel: meta.spec.backendTestLayout.markdownRel,
2552
+ scriptRel: meta.spec.backendTestLayout.scriptRel,
2553
+ }
2554
+ : undefined);
2249
2555
  try {
2250
2556
  const gate = shell.jsonArtifactGate;
2251
2557
  const sourceNodeId = gate.fallbackFromNodeIds?.length
@@ -2273,6 +2579,7 @@ export async function executeDagShellNode(input, meta) {
2273
2579
  fromNodeId: sourceNodeId,
2274
2580
  artifactName: gate.artifactName,
2275
2581
  outputDir: gate.outputDir,
2582
+ allowedTestRoot: backendLayout.testRoot,
2276
2583
  });
2277
2584
  break;
2278
2585
  case "backend-test-result-v1":
@@ -2282,6 +2589,7 @@ export async function executeDagShellNode(input, meta) {
2282
2589
  artifactName: gate.artifactName,
2283
2590
  outputDir: gate.outputDir,
2284
2591
  junitRelativePath: gate.junitRelativePath,
2592
+ testRoot: backendLayout.testRoot,
2285
2593
  });
2286
2594
  break;
2287
2595
  case "backend-test-classification-v1":
@@ -98,6 +98,13 @@ export function isEphemeralToolCachePath(filePath) {
98
98
  const normalized = normalizePath(filePath);
99
99
  if (!normalized)
100
100
  return false;
101
+ // CodeGraph extension sync side effect (plan 2026-08-21 D7): a loaded
102
+ // pi-codegraph extension keeps an existing .codegraph/ index fresh at
103
+ // session start; those index files are extension-owned run state, not
104
+ // writer output, so they never count as write-guard violations.
105
+ if (normalized === ".codegraph" || normalized.startsWith(".codegraph/")) {
106
+ return true;
107
+ }
101
108
  if (normalized === ".pytest_cache" ||
102
109
  normalized.startsWith(".pytest_cache/")) {
103
110
  return true;
@@ -78,8 +78,6 @@ export const executorManifestSchema = z
78
78
  LOW: executorTierModelSchema.optional(),
79
79
  MED: executorTierModelSchema.optional(),
80
80
  HIGH: executorTierModelSchema.optional(),
81
- /** Console 模型矩阵下拉专用列表;仅供 UI 使用,不参与 DAG 模型路由。 */
82
- modelOptions: z.array(z.string().min(1)).optional(),
83
81
  })
84
82
  .strict();
85
83
  export const workflowPolicyProfileNameSchema = z.enum([