@tea-agent/loop-agent 0.16.19 → 0.16.21

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 (30) hide show
  1. package/CHANGELOG.md +28 -5
  2. package/README.md +8 -0
  3. package/dist/cli/command-definitions.js +4 -2
  4. package/dist/cli/program.js +2 -1
  5. package/dist/cli/update/init-surface-notifier.js +167 -0
  6. package/dist/cli/update/policy.js +36 -1
  7. package/dist/cli/update/runtime-activity.js +29 -0
  8. package/dist/cli.js +14 -2
  9. package/dist/commands/init.js +85 -2
  10. package/dist/executors/shell-executor.js +92 -66
  11. package/dist/executors/shell-write-guard.js +5 -0
  12. package/dist/shared/runtime-activity.js +6 -0
  13. package/dist/worker/observability/read-model.js +10 -7
  14. package/dist/worker/observe/static/views/session-timeline.js +1 -1
  15. package/dist/workflows/dag/backend-test-case-manifest.js +13 -3
  16. package/dist/workflows/dag/backend-test-classification-contract.js +38 -0
  17. package/dist/workflows/dag/backend-test-contract-envelope.js +167 -0
  18. package/dist/workflows/dag/backend-test-semantic-review-contract.js +2 -2
  19. package/dist/workflows/dag/init-hybrid.js +64 -409
  20. package/dist/workflows/dag/node-execution.js +4 -3
  21. package/dist/workflows/dag/types.js +1 -5
  22. package/docs/README.md +1 -0
  23. package/docs/templates/agent-dag.schema.json +1 -1
  24. package/docs/templates/backend-test-case-manifest.schema.json +35 -2
  25. package/docs/templates/backend-test-dag.json +39 -340
  26. package/docs/templates/backend-test-dag.review-cases.prompt.md +4 -4
  27. package/package.json +1 -1
  28. package/skills/loop-agent/references/command-reference.md +4 -0
  29. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  30. package/dist/workflows/dag/backend-test-repair-contract.js +0 -94
@@ -18,7 +18,6 @@ import { resolveVerifyPreset } from "../../executors/shell-verification.js";
18
18
  import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
19
19
  import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
20
20
  import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
21
- import { buildBackendTestEffectiveResultSelectorShellSnippet, buildBackendTestRepairEligibilityShellSnippet, buildBackendTestRepairSafetyShellSnippet, } from "./backend-test-repair-contract.js";
22
21
  import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
23
22
  import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
24
23
  import { classifyFrontendRisk, } from "./frontend-risk.js";
@@ -900,6 +899,22 @@ function extractExplicitRequirementIds(...markdownInputs) {
900
899
  }
901
900
  return ids;
902
901
  }
902
+ /**
903
+ * Prefer TaskSpec-scoped acceptance refs from the derived 需求.md section when
904
+ * present. Full Feature acceptance.yaml may list sibling ACs that this task is
905
+ * not responsible for covering (e.g. FE-TEST must not require AC-CONTRACT-*).
906
+ */
907
+ export function extractTaskScopedRequirementIds(requirementMarkdown, ...fallbackMarkdown) {
908
+ // Only the explicit TaskSpec-derived section is authoritative for scope.
909
+ // Without it, preserve legacy full-scan behavior (requirement + references).
910
+ const sectionMatch = requirementMarkdown.match(/(?:^|\n)##\s*Acceptance References\s*\n([\s\S]*?)(?=\n##\s+|\n#\s+|$)/i);
911
+ if (sectionMatch?.[1]) {
912
+ const fromSection = extractExplicitRequirementIds(sectionMatch[1]);
913
+ if (fromSection.length > 0)
914
+ return fromSection;
915
+ }
916
+ return extractExplicitRequirementIds(requirementMarkdown, ...fallbackMarkdown);
917
+ }
903
918
  function buildDagSourceBinding(sources) {
904
919
  const sourceEntries = [
905
920
  {
@@ -932,7 +947,7 @@ function buildDagSourceBinding(sources) {
932
947
  .update(source.markdown, "utf8")
933
948
  .digest("hex"),
934
949
  })),
935
- requirementIds: extractExplicitRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
950
+ requirementIds: extractTaskScopedRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
936
951
  };
937
952
  }
938
953
  function buildBackendTestAnalysisSourceBindingContract(sources) {
@@ -2682,7 +2697,7 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
2682
2697
  }
2683
2698
  const BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS = [
2684
2699
  "The final fenced JSON block is authoritative and MUST conform exactly to Backend Test Case Manifest v1.",
2685
- "Top-level keys MUST be exactly: schemaVersion, sourceBinding, cases, evidenceGaps, and optional coverageSummary. Set schemaVersion to numeric 1. Do NOT emit schemaId, manifestType, taskId, modules, acCoverage, brCoverage, dataIsolation, readiness, or other custom top-level keys.",
2700
+ "Top-level keys MUST be exactly: schemaVersion, sourceBinding, cases, evidenceGaps. Do NOT emit coverageSummary — the shell materializer always computes it from sourceBinding/cases/evidenceGaps. Set schemaVersion to numeric 1. Do NOT emit schemaId, manifestType, taskId, modules, acCoverage, brCoverage, dataIsolation, readiness, or other custom top-level keys.",
2686
2701
  "Copy sourceBinding exactly from contracts/backend-test-analysis.json: taskId, requirementPath, requirementSha256, referencePaths, requirementIds. Preserve Unicode paths exactly; never replace characters in source/需求.md or other paths.",
2687
2702
  "Each cases[] item MUST use exactly: caseId, non-empty acIds, title, category, automationStatus; optional endpointRef, ruleRefs, file, symbol, gapReason, evidenceRef. Do NOT use id, module, brIds, endpoint, or priority.",
2688
2703
  "category MUST be exactly one of: positive, negative, boundary, state-transition, auth, timeout, concurrency, other.",
@@ -2690,7 +2705,7 @@ const BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS = [
2690
2705
  "Each evidenceGaps[] item MUST use exactly: optional acId, optional caseId, required description, optional evidenceRef. Every gap requires at least acId or caseId. Do NOT use requirementId, relatedBrIds, or sourceRef.",
2691
2706
  "Every case must map to at least one semantically applicable explicit AC-* in acIds. If no AC applies, omit that case and bind an evidence gap to the nearest applicable acId or caseId; never emit an unbound informational gap.",
2692
2707
  "acIds MUST exactly match the explicit AC-* values in the written case body; do not infer ACs from Business Rules or summary matrices.",
2693
- "Use full BE-<MODULE>-<NNN> caseId strings. Do not invent coverage percentages; omit coverageSummary unless all deterministic counts are exact.",
2708
+ "Use full BE-<MODULE>-<NNN> caseId strings. Do not invent coverage percentages and do not emit coverageSummary; shell always writes the canonical summary.",
2694
2709
  "Minimal shape example: {\"schemaVersion\":1,\"sourceBinding\":{\"taskId\":\"...\",\"requirementPath\":\"source/需求.md\",\"requirementSha256\":\"<64 lowercase hex>\",\"referencePaths\":[],\"requirementIds\":[\"AC-001\"]},\"cases\":[{\"caseId\":\"BE-MODULE-001\",\"acIds\":[\"AC-001\"],\"title\":\"...\",\"category\":\"positive\",\"automationStatus\":\"planned\",\"evidenceRef\":\"testcase/md/module.md\"}],\"evidenceGaps\":[]}",
2695
2710
  ].join("\n\n");
2696
2711
  function buildEmitBackendCaseManifestNode(sources, options) {
@@ -2769,10 +2784,8 @@ function buildBackendTestTraceabilityGateNode(sources, options = {}) {
2769
2784
  };
2770
2785
  }
2771
2786
  function buildReviewBackendCasesNode(sources, options) {
2772
- const phase = options?.phase ?? "first";
2773
- const isFinal = phase === "final";
2774
2787
  return {
2775
- id: options?.id ?? "review-backend-cases-pi",
2788
+ id: "review-backend-cases-pi",
2776
2789
  depends_on: options?.dependsOn ?? [
2777
2790
  "backend-test-case-manifest-shell",
2778
2791
  "backend-test-analysis-contract-shell",
@@ -2783,13 +2796,9 @@ function buildReviewBackendCasesNode(sources, options) {
2783
2796
  writePolicy: "read-only",
2784
2797
  allowedPaths: commonReadOnlyPaths(sources),
2785
2798
  forbiddenPaths: commonForbiddenPaths(sources),
2786
- outputContract: isFinal
2787
- ? "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision after at most one revision; followed by Findings and Coverage Assessment. No file writes. Final gate accepts pass only."
2788
- : "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; followed by Findings, Coverage Assessment, and Required revisions when requesting revision. No file writes.",
2799
+ outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; followed by Findings and Coverage Assessment. No file writes. The deterministic gate accepts pass only; request-revision ends this run.",
2789
2800
  subtask_prompt: [
2790
- isFinal
2791
- ? "Final review of backend functional cases under testcase/md/ and the re-validated Case Manifest v1 after at most one revision pass."
2792
- : "Review the generated backend functional test cases under testcase/md/ and the validated Case Manifest v1.",
2801
+ "Review the generated backend functional test cases under testcase/md/ and the validated Case Manifest v1.",
2793
2802
  "",
2794
2803
  "## Mandatory First Line:",
2795
2804
  "First non-empty line must be exactly: VERDICT: pass or VERDICT: request-revision",
@@ -2818,15 +2827,13 @@ function buildReviewBackendCasesNode(sources, options) {
2818
2827
  "## Verdict Rules:",
2819
2828
  "- All Critical checks pass + Important findings ≤ 2 → VERDICT: pass",
2820
2829
  "- Any Critical fails OR Important > 2 → VERDICT: request-revision",
2821
- isFinal
2822
- ? "- This is the FINAL review after one revision opportunity; remaining Critical issues must still request-revision (final gate will stop the DAG)."
2823
- : "- When requesting revision, list numbered Required revisions concrete enough for revise-backend-cases-pi to edit testcase/md/**.",
2830
+ "- Any request-revision verdict ends the current run at the deterministic gate; describe findings clearly for an independent follow-up task.",
2824
2831
  "",
2825
2832
  "## Output After Verdict:",
2826
2833
  "1. Coverage Assessment table (AC → full BE-* case IDs) using manifest + MD",
2827
2834
  "2. Findings list (Critical/Important/Informational)",
2828
2835
  "3. Statistics (total cases, positive/negative/boundary breakdown)",
2829
- isFinal ? "" : "4. Required revisions (only when request-revision)",
2836
+ "4. Required follow-up actions (only when request-revision; no in-run writer)",
2830
2837
  "",
2831
2838
  "## Constraints:",
2832
2839
  "- Read-only: do not modify files",
@@ -2837,69 +2844,6 @@ function buildReviewBackendCasesNode(sources, options) {
2837
2844
  .join("\n\n"),
2838
2845
  };
2839
2846
  }
2840
- function buildReviewBackendCasesBranchConditionNode(sources) {
2841
- return {
2842
- id: "review-backend-cases-branch-condition",
2843
- depends_on: ["review-backend-cases-pi"],
2844
- role: "verifier",
2845
- executor: "static",
2846
- complexity: "LOW",
2847
- writePolicy: "none",
2848
- allowedPaths: commonReadOnlyPaths(sources),
2849
- forbiddenPaths: commonForbiddenPaths(sources),
2850
- outputContract: "Dynamic condition: select direct pass vs single revision chain from first review firstVerdictLine; malformed VERDICT fails closed (no default). The pass target is the already-finished first review, so the effective gate remains outside the exclusive branch targets.",
2851
- subtask_prompt: "Branch on review-backend-cases-pi VERDICT: pass → already-finished first review (revision chain skipped); request-revision → revise-backend-cases-pi.",
2852
- static: {
2853
- resultMarkdown: "Backend case review branch condition (direct pass vs single revision).",
2854
- },
2855
- dynamicCondition: {
2856
- workflowNodeId: "review-backend-cases-branch-condition",
2857
- cases: [
2858
- {
2859
- when: "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: pass'",
2860
- then: "review-backend-cases-pi",
2861
- },
2862
- {
2863
- when: "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'",
2864
- then: "revise-backend-cases-pi",
2865
- },
2866
- ],
2867
- },
2868
- };
2869
- }
2870
- function buildReviseBackendCasesNode(sources) {
2871
- return {
2872
- id: "revise-backend-cases-pi",
2873
- depends_on: ["review-backend-cases-branch-condition"],
2874
- role: "implementer",
2875
- executor: "pi",
2876
- toolProfile: "write",
2877
- complexity: "MED",
2878
- writePolicy: "exclusive",
2879
- writeSet: ["testcase/md/**"],
2880
- allowedPaths: ["testcase/md/**"],
2881
- forbiddenPaths: commonForbiddenPaths(sources),
2882
- outputContract: "Only scheduled when first review is VERDICT: request-revision. Apply Required revisions under testcase/md/** then summarize changes (single revision pass).",
2883
- subtask_prompt: [
2884
- "You are the single backend case revision pass (max one per DAG run).",
2885
- "This node is only scheduled when review-backend-cases-pi emitted VERDICT: request-revision.",
2886
- "",
2887
- "## Inputs",
2888
- "- First review: review-backend-cases-pi Markdown (VERDICT + Findings + Required revisions)",
2889
- "- Current cases: testcase/md/**",
2890
- "- Validated analysis + case manifest under the current run contracts/",
2891
- "",
2892
- "## Required work",
2893
- "1. Edit only testcase/md/** to address every Critical finding and Required revision item",
2894
- "2. Fix AC matrices to list FULL BE-* ids matching case bodies; remove false 'all cases' AC claims",
2895
- "3. Keep BE-<MODULE>-<NNN> ids stable when possible; do not invent out-of-scope AC coverage",
2896
- "4. Stay within writeSet; do not write pytest or production code",
2897
- "5. End with a short summary of files touched",
2898
- "",
2899
- "Downstream will re-emit and re-validate Case Manifest v1, then run a FINAL review (pass-only gate).",
2900
- ].join("\n\n"),
2901
- };
2902
- }
2903
2847
  function buildReviewBackendCasesGateNode(sources) {
2904
2848
  return {
2905
2849
  id: "review-backend-cases-gate-shell",
@@ -3053,21 +2997,18 @@ function buildGenerateBackendPytestNode(sources) {
3053
2997
  "- If a test filename exists, add suffix: test_order.py → test_order_01.py",
3054
2998
  "- Do NOT re-read source documents — use reviewed cases under testcase/md/ and upstream analyze-inputs-pi output only",
3055
2999
  "- Read existing conftest.py/pytest.ini to understand conventions, but do NOT modify them",
3056
- "- Do NOT execute pytest/python -m pytest or npm test in this node; initial/final execution is owned by dedicated shell nodes. Local smoke runs create __pycache__/.pytest_cache and are unnecessary here.",
3000
+ "- Do NOT execute pytest/python -m pytest or npm test in this node; the single execution is owned by the dedicated shell node. Local smoke runs create __pycache__/.pytest_cache and are unnecessary here.",
3057
3001
  ].join("\n\n"),
3058
3002
  };
3059
3003
  }
3060
3004
  function buildBackendTestSemanticReviewNode(sources, options = {}) {
3061
- const final = options.final ?? false;
3062
3005
  return {
3063
- id: options.id ?? "review-generated-backend-pytest-pi",
3006
+ id: "review-generated-backend-pytest-pi",
3064
3007
  depends_on: options.dependsOn ?? [
3065
3008
  "generate-backend-pytest-pi",
3066
3009
  "backend-test-analysis-contract-shell",
3067
3010
  "backend-test-case-manifest-shell",
3068
- "backend-test-case-manifest-final-shell",
3069
3011
  ],
3070
- dependsPolicy: "all-or-condition-skip",
3071
3012
  role: "reviewer",
3072
3013
  executor: "pi",
3073
3014
  complexity: "MED",
@@ -3076,7 +3017,7 @@ function buildBackendTestSemanticReviewNode(sources, options = {}) {
3076
3017
  forbiddenPaths: commonForbiddenPaths(sources),
3077
3018
  outputContract: "Pure Backend Test Semantic Review v1 JSON: verdict, findings[], summary. No file writes.",
3078
3019
  subtask_prompt: [
3079
- final ? "Final semantic review after the single generated-pytest revision." : "Review generated pytest semantics before the first execution.",
3020
+ "Review generated pytest semantics before the single execution.",
3080
3021
  "Use only compact authoritative inputs: contracts/backend-test-analysis.json, contracts/backend-test-case-manifest.json, testcase/md/**, and generated testcase/**/test_*.py/helpers/factories.",
3081
3022
  "Return exactly one pure JSON object with only verdict, findings, summary; no Markdown fence or surrounding prose.",
3082
3023
  "verdict must be pass or request-revision. Each findings[] item must contain exactly severity, caseId, testFile, testSymbol, contractRefs, issue, requiredChange.",
@@ -3089,98 +3030,6 @@ function buildBackendTestSemanticReviewNode(sources, options = {}) {
3089
3030
  ].join("\n\n"),
3090
3031
  };
3091
3032
  }
3092
- function buildBackendTestSemanticReviewGateNode(sources, options = {}) {
3093
- const fromNodeId = options.fromNodeId ?? "review-generated-backend-pytest-pi";
3094
- return {
3095
- id: options.id ?? "backend-test-semantic-review-shell",
3096
- depends_on: options.dependsOn ?? [fromNodeId],
3097
- role: "verifier",
3098
- executor: "shell",
3099
- complexity: "LOW",
3100
- writePolicy: "read-only",
3101
- allowedPaths: commonReadOnlyPaths(sources),
3102
- forbiddenPaths: commonForbiddenPaths(sources),
3103
- outputContract: "Validated run-owned Backend Test Semantic Review v1 artifact.",
3104
- subtask_prompt: "Validate semantic review JSON before branch selection.",
3105
- shell: {
3106
- commands: [],
3107
- jsonArtifactGate: {
3108
- fromNodeId,
3109
- schemaId: "backend-test-semantic-review-v1",
3110
- artifactName: options.id?.includes("final") ? "backend-test-semantic-review-final.json" : "backend-test-semantic-review.json",
3111
- outputDir: "contracts",
3112
- },
3113
- cwd: ".",
3114
- timeoutMs: 60000,
3115
- },
3116
- };
3117
- }
3118
- function buildBackendTestSemanticReviewConditionNode(sources) {
3119
- return {
3120
- id: "backend-test-semantic-review-condition",
3121
- depends_on: ["backend-test-semantic-review-shell"],
3122
- role: "verifier",
3123
- executor: "static",
3124
- complexity: "LOW",
3125
- writePolicy: "none",
3126
- allowedPaths: [],
3127
- forbiddenPaths: commonForbiddenPaths(sources),
3128
- outputContract: "Select direct semantic pass or one testcase-only revision; the pass target is the already-finished validated semantic review so the final gate remains outside the exclusive branch targets.",
3129
- subtask_prompt: "Branch deterministically from semantic review verdict.",
3130
- static: { resultMarkdown: "Backend pytest semantic review selector." },
3131
- dynamicCondition: {
3132
- workflowNodeId: "backend-test-semantic-review-condition",
3133
- cases: [
3134
- { when: "$.nodes['backend-test-semantic-review-shell'].json.verdict == 'pass'", then: "backend-test-semantic-review-shell" },
3135
- ],
3136
- default: "revise-generated-backend-pytest-pi",
3137
- },
3138
- };
3139
- }
3140
- function buildReviseGeneratedBackendPytestNode(sources) {
3141
- const writeSet = ["testcase/**/test_*.py", "testcase/**/helpers/**", "testcase/**/factories/**"];
3142
- return {
3143
- id: "revise-generated-backend-pytest-pi",
3144
- depends_on: ["backend-test-semantic-review-condition"],
3145
- role: "implementer",
3146
- executor: "pi",
3147
- toolProfile: "write",
3148
- complexity: "MED",
3149
- writePolicy: "exclusive",
3150
- writeSet,
3151
- allowedPaths: writeSet,
3152
- forbiddenPaths: [...commonForbiddenPaths(sources), "apps/**", "src/**", "testcase/md/**", "**/conftest.py", "**/pytest.ini"],
3153
- outputContract: "Apply one bounded semantic correction to generated pytest from validated findings only.",
3154
- subtask_prompt: [
3155
- "This is the only pre-execution generated-pytest semantic revision (1/1).",
3156
- "Read contracts/backend-test-semantic-review.json and edit only the cited generated test files/helpers/factories.",
3157
- "Preserve case IDs, symbols, test count, target mode, base URL and real-service path.",
3158
- "Do not delete tests, add skip/xfail, swallow failures, substitute mocks, or weaken assertions beyond the formal comparison/shape contract.",
3159
- "Do not modify product code, testcase/md/**, conftest.py, pytest.ini, or .harness/**.",
3160
- ].join("\n\n"),
3161
- };
3162
- }
3163
- function buildBackendTestSemanticFinalGateNode(sources) {
3164
- return {
3165
- id: "backend-test-semantic-final-gate-shell",
3166
- depends_on: ["backend-test-semantic-review-condition", "backend-test-semantic-review-final-shell"],
3167
- dependsPolicy: "all-or-condition-skip",
3168
- role: "verifier",
3169
- executor: "shell",
3170
- complexity: "LOW",
3171
- writePolicy: "read-only",
3172
- allowedPaths: commonReadOnlyPaths(sources),
3173
- forbiddenPaths: commonForbiddenPaths(sources),
3174
- outputContract: "Pass-only semantic authorization for traceability and initial pytest.",
3175
- subtask_prompt: "Accept initial pass path or final semantic review pass; fail closed otherwise.",
3176
- shell: {
3177
- commands: [],
3178
- backendTestPipeline: "semantic-effective",
3179
- cwd: ".",
3180
- timeoutMs: 60000,
3181
- },
3182
- };
3183
- }
3184
3033
  function collectBackendTestShellEnvAllowlist(sources) {
3185
3034
  const names = new Set();
3186
3035
  for (const verify of sources.taskConfig.verifyCommands) {
@@ -3228,7 +3077,7 @@ function buildExecuteBackendPytestNode(sources, options = {}) {
3228
3077
  return {
3229
3078
  id: nodeId,
3230
3079
  depends_on: options.dependsOn ?? [
3231
- "backend-test-semantic-final-gate-shell",
3080
+ "backend-test-semantic-gate-shell",
3232
3081
  "backend-test-execution-contract-shell",
3233
3082
  ],
3234
3083
  role: "verifier",
@@ -3254,41 +3103,10 @@ function buildExecuteBackendPytestNode(sources, options = {}) {
3254
3103
  },
3255
3104
  };
3256
3105
  }
3257
- function buildParseBackendTestResultNode(sources, options = {}) {
3258
- const id = options.id ?? "parse-backend-test-result-shell";
3259
- const fromNodeId = options.fromNodeId ?? "execute-backend-pytest-shell";
3260
- const artifactName = options.artifactName ?? "backend-test-result.json";
3261
- return {
3262
- id,
3263
- depends_on: options.dependsOn ?? [fromNodeId],
3264
- role: "verifier",
3265
- executor: "shell",
3266
- complexity: "LOW",
3267
- writePolicy: "read-only",
3268
- allowedPaths: commonReadOnlyPaths(sources),
3269
- forbiddenPaths: commonForbiddenPaths(sources),
3270
- outputContract: "Validated run-owned Backend Test Result v1 at contracts/backend-test-result.json (schemaId backend-test-result-v1) with outcome/counts/failures from deterministic JUnit parse.",
3271
- subtask_prompt: "Materialize Backend Test Result v1 from JUnit + pytestExitCode under the current DAG run (fail-closed on missing/corrupt report).",
3272
- shell: {
3273
- commands: [],
3274
- jsonArtifactGate: {
3275
- fromNodeId,
3276
- schemaId: "backend-test-result-v1",
3277
- artifactName,
3278
- outputDir: "contracts",
3279
- ...(options.junitRelativePath
3280
- ? { junitRelativePath: options.junitRelativePath }
3281
- : {}),
3282
- },
3283
- cwd: ".",
3284
- timeoutMs: 60000,
3285
- },
3286
- };
3287
- }
3288
3106
  function buildClassifyBackendTestResultNode(sources) {
3289
3107
  return {
3290
3108
  id: "classify-backend-test-result-pi",
3291
- depends_on: ["parse-backend-test-result-initial-shell"],
3109
+ depends_on: ["execute-and-parse-backend-pytest-shell"],
3292
3110
  role: "reviewer",
3293
3111
  executor: "pi",
3294
3112
  complexity: "MED",
@@ -3313,155 +3131,6 @@ function buildClassifyBackendTestResultNode(sources) {
3313
3131
  ].join("\n\n"),
3314
3132
  };
3315
3133
  }
3316
- function buildBackendTestClassificationGateNode(sources) {
3317
- return {
3318
- id: "backend-test-classification-shell",
3319
- depends_on: ["classify-backend-test-result-pi"],
3320
- role: "verifier",
3321
- executor: "shell",
3322
- complexity: "LOW",
3323
- writePolicy: "read-only",
3324
- allowedPaths: commonReadOnlyPaths(sources),
3325
- forbiddenPaths: commonForbiddenPaths(sources),
3326
- outputContract: "Validated run-owned Backend Test Classification v1 at contracts/backend-test-classification.json.",
3327
- subtask_prompt: "Validate and materialize the read-only backend-test classification for deterministic repair routing.",
3328
- shell: {
3329
- commands: [],
3330
- jsonArtifactGate: {
3331
- fromNodeId: "classify-backend-test-result-pi",
3332
- schemaId: "backend-test-classification-v1",
3333
- artifactName: "backend-test-classification.json",
3334
- outputDir: "contracts",
3335
- },
3336
- cwd: ".",
3337
- timeoutMs: 60000,
3338
- },
3339
- };
3340
- }
3341
- function buildBackendTestRepairEligibilityNode(sources) {
3342
- const command = buildBackendTestRepairEligibilityShellSnippet();
3343
- return {
3344
- id: "backend-test-repair-eligibility-shell",
3345
- depends_on: ["backend-test-classification-shell"],
3346
- role: "verifier",
3347
- executor: "shell",
3348
- complexity: "LOW",
3349
- writePolicy: "read-only",
3350
- allowedPaths: commonReadOnlyPaths(sources),
3351
- forbiddenPaths: commonForbiddenPaths(sources),
3352
- outputContract: "Pure JSON {schemaVersion,eligible,reason,revisionAttempt,category,confidence}; eligible only for completed TestBug assertion failures with error=0.",
3353
- subtask_prompt: "Deterministically decide whether this run may use its single testcase-only repair attempt.",
3354
- shell: { commands: [command], cwd: ".", timeoutMs: 60000 },
3355
- };
3356
- }
3357
- function buildBackendTestRepairConditionNode(sources) {
3358
- return {
3359
- id: "backend-test-repair-condition",
3360
- depends_on: ["backend-test-repair-eligibility-shell"],
3361
- role: "verifier",
3362
- executor: "static",
3363
- complexity: "LOW",
3364
- writePolicy: "none",
3365
- allowedPaths: [],
3366
- forbiddenPaths: commonForbiddenPaths(sources),
3367
- outputContract: "Select repair or skip repair exactly once; ineligible runs select the already-finished eligibility evidence so the effective-result shell remains outside the exclusive branch targets.",
3368
- subtask_prompt: "Route eligible TestBug to one repair attempt; otherwise select the already-finished eligibility evidence and skip repair.",
3369
- static: { resultMarkdown: "Backend-test repair branch selector." },
3370
- dynamicCondition: {
3371
- workflowNodeId: "backend-test-repair-condition",
3372
- cases: [
3373
- {
3374
- when: "$.nodes['backend-test-repair-eligibility-shell'].json.eligible == true",
3375
- then: "repair-backend-pytest-pi",
3376
- },
3377
- ],
3378
- default: "backend-test-repair-eligibility-shell",
3379
- },
3380
- };
3381
- }
3382
- function buildRepairBackendPytestNode(sources) {
3383
- const writeSet = [
3384
- "testcase/**/test_*.py",
3385
- "testcase/**/helpers/**",
3386
- "testcase/**/factories/**",
3387
- ];
3388
- return {
3389
- id: "repair-backend-pytest-pi",
3390
- depends_on: ["backend-test-repair-condition"],
3391
- role: "implementer",
3392
- executor: "pi",
3393
- toolProfile: "write",
3394
- complexity: "HIGH",
3395
- writePolicy: "exclusive",
3396
- writeSet,
3397
- allowedPaths: writeSet,
3398
- forbiddenPaths: [
3399
- ...commonForbiddenPaths(sources),
3400
- "apps/**",
3401
- "src/**",
3402
- ".env*",
3403
- "**/migrations/**",
3404
- ],
3405
- outputContract: "Repair only existing generated pytest tests/helpers/factories for classified TestBug findings. No production/config/runtime evidence writes.",
3406
- subtask_prompt: [
3407
- "This is the only automatic TestBug repair attempt (1/1).",
3408
- "Read contracts/backend-test-result-initial.json, contracts/backend-test-classification.json, contracts/backend-test-analysis.json, contracts/backend-test-case-manifest.json, and only the generated pytest files named in failures[].name or manifest mappings.",
3409
- "Do not re-read the full task source tree: the run-owned analysis/manifest are the compact authoritative context.",
3410
- "Repair only test implementation defects directly supported by reviewed Expected Results or formal API contract evidence.",
3411
- "Never modify product code, migrations, service configuration, conftest.py, pytest.ini, pyproject.toml, setup.cfg, source requirements, testcase/md/**, or .harness/**.",
3412
- "Never delete a test, remove a case mapping, add skip/skipif/xfail, swallow AssertionError/network exceptions, switch to a mock server, or weaken documented status/value assertions.",
3413
- "Keep the full suite runnable and preserve each BE-* case ID and pytest symbol mapping.",
3414
- "Stay within writeSet: testcase/**/test_*.py, helpers/**, factories/**.",
3415
- ].join("\n\n"),
3416
- };
3417
- }
3418
- function buildBackendTestRepairSafetyGateNode(sources) {
3419
- const command = buildBackendTestRepairSafetyShellSnippet();
3420
- return {
3421
- id: "backend-test-repair-safety-gate-shell",
3422
- depends_on: ["repair-backend-pytest-pi"],
3423
- role: "verifier",
3424
- executor: "shell",
3425
- complexity: "LOW",
3426
- writePolicy: "read-only",
3427
- allowedPaths: ["testcase/**"],
3428
- forbiddenPaths: commonForbiddenPaths(sources),
3429
- outputContract: "Fail-closed repair safety gate rejecting skip/xfail and broad failure swallowing before final traceability and pytest.",
3430
- subtask_prompt: "Check that automatic TestBug repair did not manufacture success.",
3431
- shell: { commands: [command], cwd: ".", timeoutMs: 60000 },
3432
- };
3433
- }
3434
- function buildBackendTestEffectiveResultNode(sources) {
3435
- const command = buildBackendTestEffectiveResultSelectorShellSnippet();
3436
- // Inline retrospective context: relocated verbatim from the former
3437
- // buildBackendTestRetrospectiveContextNode so test-retrospect-pi keeps a
3438
- // single upstream. Runs after `command` which materializes
3439
- // contracts/backend-test-result.json from the effective (final|initial) result.
3440
- const retroCommand = [
3441
- 'test -n "${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for backend-test retrospective context" >&2; exit 2; }',
3442
- 'node -e \'const fs=require("fs"),path=require("path"); const root=process.env.HARNESS_DAG_RUN_DIR; const read=(name)=>JSON.parse(fs.readFileSync(path.join(root,"contracts",name),"utf8")); process.stdout.write(JSON.stringify({schemaVersion:1,result:read("backend-test-result.json"),initialResult:read("backend-test-result-initial.json"),finalResult:fs.existsSync(path.join(root,"contracts","backend-test-result-final.json"))?read("backend-test-result-final.json"):null,manifest:read("backend-test-case-manifest.json"),classification:read("backend-test-classification.json")},null,2));\'',
3443
- ].join("; ");
3444
- return {
3445
- id: "select-effective-backend-test-result-shell",
3446
- depends_on: [
3447
- "backend-test-repair-condition",
3448
- "parse-backend-test-result-final-shell",
3449
- "backend-test-case-manifest-shell",
3450
- "backend-test-case-manifest-final-shell",
3451
- "backend-test-classification-shell",
3452
- ],
3453
- dependsPolicy: "all-or-condition-skip",
3454
- role: "verifier",
3455
- executor: "shell",
3456
- complexity: "LOW",
3457
- writePolicy: "read-only",
3458
- allowedPaths: commonReadOnlyPaths(sources),
3459
- forbiddenPaths: commonForbiddenPaths(sources),
3460
- outputContract: "Materialize contracts/backend-test-result.json from final result when present, otherwise initial result; emit effective result metadata, and emit the complete retrospective context (Result v1 + Case Manifest v1 + classifier output) for downstream retrospective consumption.",
3461
- subtask_prompt: "Select the effective backend-test result deterministically without changing initial/final evidence, then emit the complete retrospective context.",
3462
- shell: { commands: [command, retroCommand], cwd: ".", timeoutMs: 60000 },
3463
- };
3464
- }
3465
3134
  function buildTestRetrospectNode(sources) {
3466
3135
  const canWriteReport = taskAllowsBackendTestReportWrite(sources);
3467
3136
  return {
@@ -3567,10 +3236,11 @@ function buildBackendTestHybridDag(sources) {
3567
3236
  const globalConstraints = [
3568
3237
  ...taskConfig.hardConstraints,
3569
3238
  ...STANDARD_GLOBAL_CONSTRAINTS,
3570
- "backend-test-dag uses exactly 24 real top-level tasks; bounded revision/repair branches are controlled by fail-closed runIf expressions.",
3571
- "Analysis, execution, manifest, semantic review, initial/final/effective results, classification, eligibility, repair safety, traceability and outcome evidence remain run-owned and fail-closed.",
3239
+ "backend-test-dag uses exactly 15 real top-level tasks and executes pytest exactly once.",
3240
+ "Case and semantic request-revision verdicts fail at deterministic gates; no in-run revision or repair writer is authorized.",
3241
+ "Analysis, execution, manifest, semantic review, single-run result, classification, canonical result, retrospective and outcome evidence remain run-owned and fail-closed.",
3572
3242
  "Functional test case IDs must use BE-<MODULE>-<NNN> format.",
3573
- "pytest writers may only change declared testcase assets; production code, config, skip/xfail, swallowed failures and mock substitution are forbidden.",
3243
+ "pytest writers may only create the initially declared testcase assets; production code, config, skip/xfail, swallowed failures and mock substitution are forbidden.",
3574
3244
  ];
3575
3245
  const analyze = buildAnalyzeInputsNode(sources);
3576
3246
  analyze.id = "analyze-and-discover-backend-test-pi";
@@ -3589,64 +3259,49 @@ function buildBackendTestHybridDag(sources) {
3589
3259
  generateCases.subtask_prompt += `\n\nAfter writing Markdown, end assistant output with exactly one fenced json block containing Backend Test Case Manifest v1 derived from the written cases.\n\n${BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS}`;
3590
3260
  const manifest = buildBackendTestCaseManifestGateNode(sources, { dependsOn: [generateCases.id], fromNodeId: generateCases.id });
3591
3261
  const reviewCases = buildReviewBackendCasesNode(sources, { dependsOn: [manifest.id, contracts.id] });
3592
- const reviseCases = buildReviseBackendCasesNode(sources);
3593
- reviseCases.depends_on = [reviewCases.id];
3594
- reviseCases.runIf = "$.nodes['review-backend-cases-pi'].firstVerdictLine == 'VERDICT: request-revision'";
3595
- reviseCases.outputContract = "Apply one case revision and end with one fenced json final Case Manifest v1 block matching the strict field contract.";
3596
- reviseCases.subtask_prompt += `\n\nAfter edits, end assistant output with exactly one fenced json block containing the complete final Backend Test Case Manifest v1.\n\n${BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS}`;
3597
- const finalManifest = buildBackendTestCaseManifestGateNode(sources, { id: "backend-test-case-manifest-final-shell", dependsOn: [reviseCases.id], fromNodeId: reviseCases.id });
3598
- finalManifest.runIf = reviseCases.runIf;
3599
- const finalCaseReview = buildReviewBackendCasesNode(sources, { id: "review-backend-cases-final-pi", phase: "final", dependsOn: [finalManifest.id, contracts.id] });
3600
- finalCaseReview.runIf = reviseCases.runIf;
3601
3262
  const caseGate = buildReviewBackendCasesGateNode(sources);
3602
- caseGate.depends_on = [reviewCases.id, finalCaseReview.id];
3603
- caseGate.shell.verdictGate = { fromNodeId: finalCaseReview.id, fallbackFromNodeIds: [reviewCases.id], accept: ["VERDICT: pass"], label: "backend case effective review", lineMode: "first-verdict-line" };
3263
+ caseGate.depends_on = [reviewCases.id];
3264
+ caseGate.dependsPolicy = undefined;
3265
+ caseGate.outputContract = "Deterministic backend case review gate: exit 0 only when the first and only review emits VERDICT: pass.";
3266
+ caseGate.subtask_prompt = "Block pytest generation when backend case review requests revision; do not authorize an in-run writer.";
3267
+ caseGate.shell.verdictGate = { fromNodeId: reviewCases.id, accept: ["VERDICT: pass"], label: "backend case review", lineMode: "first-verdict-line" };
3604
3268
  const generatePytest = buildGenerateBackendPytestNode(sources);
3605
3269
  generatePytest.depends_on = [caseGate.id, contracts.id];
3606
- const semanticReview = buildBackendTestSemanticReviewNode(sources, { dependsOn: [generatePytest.id, contracts.id, manifest.id, finalManifest.id] });
3607
- const semanticInitial = { id: "validate-semantic-review-and-traceability-shell", depends_on: [semanticReview.id, manifest.id, finalManifest.id], dependsPolicy: "all-or-condition-skip", role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), outputContract: "Materialize initial semantic review and validate pytest traceability.", subtask_prompt: "Validate initial semantic review and traceability fail-closed.", shell: { commands: [], backendTestPipeline: "semantic-initial", cwd: ".", timeoutMs: 60000 } };
3608
- const revisePytest = buildReviseGeneratedBackendPytestNode(sources);
3609
- revisePytest.depends_on = [semanticInitial.id];
3610
- revisePytest.runIf = "$.nodes['validate-semantic-review-and-traceability-shell'].json.verdict == 'request-revision'";
3611
- const finalSemanticReview = buildBackendTestSemanticReviewNode(sources, { id: "review-generated-backend-pytest-final-pi", dependsOn: [revisePytest.id], final: true });
3612
- finalSemanticReview.runIf = revisePytest.runIf;
3613
- const finalSemanticMaterialize = { id: "materialize-final-semantic-review-shell", depends_on: [finalSemanticReview.id], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), runIf: revisePytest.runIf, outputContract: "Materialize final semantic review and re-check traceability.", subtask_prompt: "Validate final semantic review and traceability.", shell: { commands: [], backendTestPipeline: "semantic-final", cwd: ".", timeoutMs: 60000 } };
3614
- const semanticFinal = {
3615
- id: "backend-test-semantic-final-gate-shell",
3616
- depends_on: [semanticInitial.id, finalSemanticMaterialize.id],
3617
- dependsPolicy: "all-or-condition-skip",
3618
- role: "verifier",
3619
- executor: "shell",
3620
- complexity: "LOW",
3621
- writePolicy: "read-only",
3622
- allowedPaths: commonReadOnlyPaths(sources),
3623
- forbiddenPaths: commonForbiddenPaths(sources),
3624
- outputContract: "Pass-only effective semantic review gate with final traceability after revision.",
3625
- subtask_prompt: "Accept initial semantic pass or validate the single final review and traceability.",
3270
+ const semanticReview = buildBackendTestSemanticReviewNode(sources, { dependsOn: [generatePytest.id, contracts.id, manifest.id] });
3271
+ const semanticMaterialize = {
3272
+ id: "validate-semantic-review-and-traceability-shell", depends_on: [semanticReview.id, manifest.id], role: "verifier", executor: "shell", complexity: "LOW",
3273
+ writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources),
3274
+ outputContract: "Materialize the only semantic review and validate pytest traceability.", subtask_prompt: "Materialize semantic facts and traceability; verdict authorization is handled by the next deterministic gate.",
3275
+ shell: { commands: [], backendTestPipeline: "semantic-initial", cwd: ".", timeoutMs: 60000 },
3276
+ };
3277
+ const semanticGate = {
3278
+ id: "backend-test-semantic-gate-shell", depends_on: [semanticMaterialize.id, semanticReview.id], role: "verifier", executor: "shell", complexity: "LOW",
3279
+ writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources),
3280
+ outputContract: "Pass-only authorization by reading contracts/backend-test-semantic-review.json; only verdict=pass proceeds to the single pytest execution.",
3281
+ subtask_prompt: "Read the canonical semantic review artifact written by validate-semantic-review-and-traceability-shell. Authorize only when verdict is pass. Do not materialize, do not parse raw Pi Markdown or VERDICT lines, and do not authorize an in-run pytest writer.",
3626
3282
  shell: {
3627
3283
  commands: [],
3628
- backendTestPipeline: "semantic-effective",
3284
+ backendTestPipeline: "semantic-initial",
3629
3285
  cwd: ".",
3630
3286
  timeoutMs: 60000,
3631
3287
  },
3632
3288
  };
3633
- const executeInitial = buildExecuteBackendPytestNode(sources, { id: "execute-and-parse-backend-pytest-initial-shell", dependsOn: [semanticFinal.id, contracts.id], reportStem: "backend-test-initial" });
3634
- executeInitial.shell.backendTestPipeline = "execute-parse-initial";
3289
+ const execute = buildExecuteBackendPytestNode(sources, { id: "execute-and-parse-backend-pytest-shell", dependsOn: [semanticGate.id, contracts.id], reportStem: "backend-test-initial" });
3290
+ execute.shell.backendTestPipeline = "execute-parse-initial";
3635
3291
  const classify = buildClassifyBackendTestResultNode(sources);
3636
- classify.depends_on = [executeInitial.id];
3637
- const classifyEligibility = { id: "materialize-classification-and-eligibility-shell", depends_on: [classify.id], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources), outputContract: "Materialize Classification v1 and repair eligibility JSON.", subtask_prompt: "Validate classification and determine bounded repair eligibility.", shell: { commands: [], backendTestPipeline: "classification-eligibility", cwd: ".", timeoutMs: 60000 } };
3638
- const repair = buildRepairBackendPytestNode(sources);
3639
- repair.depends_on = [classifyEligibility.id];
3640
- repair.runIf = "$.nodes['materialize-classification-and-eligibility-shell'].json.eligible == true";
3641
- const repairVerify = { id: "validate-repair-safety-and-traceability-shell", depends_on: [repair.id], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: ["testcase/**"], forbiddenPaths: commonForbiddenPaths(sources), runIf: repair.runIf, outputContract: "Validate repair safety and final traceability.", subtask_prompt: "Reject manufactured success and revalidate mapping.", shell: { commands: [], backendTestPipeline: "repair-safety-traceability", cwd: ".", timeoutMs: 60000 } };
3642
- const finalize = buildExecuteBackendPytestNode(sources, { id: "finalize-effective-backend-test-result-shell", dependsOn: [classifyEligibility.id, repairVerify.id, contracts.id], reportStem: "backend-test-final" });
3643
- finalize.dependsPolicy = "all-or-condition-skip";
3644
- finalize.shell.backendTestPipeline = "finalize-effective-result";
3645
- finalize.outputContract = "If repaired, execute/parse final pytest; always materialize contracts/backend-test-result.json from final or initial result.";
3292
+ classify.depends_on = [execute.id];
3293
+ const context = {
3294
+ id: "materialize-classification-and-result-context-shell", depends_on: [classify.id, manifest.id], role: "verifier", executor: "shell", complexity: "LOW",
3295
+ writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources),
3296
+ outputContract: "Materialize Classification v1, copy the unique initial Result to canonical contracts/backend-test-result.json, and emit Result + Manifest + Classification context.",
3297
+ subtask_prompt: "Validate classification and materialize canonical single-run result context without repair eligibility or rerun.",
3298
+ shell: { commands: [], backendTestPipeline: "classification-result-context", cwd: ".", timeoutMs: 60000 },
3299
+ };
3646
3300
  const retrospect = buildTestRetrospectNode(sources);
3647
- retrospect.depends_on = [finalize.id];
3301
+ retrospect.depends_on = [context.id];
3302
+ retrospect.subtask_prompt = retrospect.subtask_prompt.replaceAll("select-effective-backend-test-result-shell", context.id);
3648
3303
  const outcome = buildBackendTestOutcomeGateNode(sources);
3649
- const tasks = [analyze, contracts, generateCases, manifest, reviewCases, reviseCases, finalManifest, finalCaseReview, caseGate, generatePytest, semanticReview, semanticInitial, revisePytest, finalSemanticReview, finalSemanticMaterialize, semanticFinal, executeInitial, classify, classifyEligibility, repair, repairVerify, finalize, retrospect, outcome];
3304
+ const tasks = [analyze, contracts, generateCases, manifest, reviewCases, caseGate, generatePytest, semanticReview, semanticMaterialize, semanticGate, execute, classify, context, retrospect, outcome];
3650
3305
  const spec = { version: 3, title: `Backend test DAG: ${taskConfig.title}`, runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT, outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE, objective: extractObjective(sources.requirementMarkdown, taskConfig.title), successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId), globalConstraints, defaults: { ...BACKEND_TEST_DEFAULTS, contextProfile: taskConfig.contextProfile }, skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE, executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS, tasks };
3651
3306
  applyDefaultReadOnlyRetryPolicy(spec);
3652
3307
  parseDagSpec(spec);