@tea-agent/loop-agent 0.16.19 → 0.16.20

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 +22 -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 +47 -408
  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";
@@ -2682,7 +2681,7 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
2682
2681
  }
2683
2682
  const BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS = [
2684
2683
  "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.",
2684
+ "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
2685
  "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
2686
  "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
2687
  "category MUST be exactly one of: positive, negative, boundary, state-transition, auth, timeout, concurrency, other.",
@@ -2690,7 +2689,7 @@ const BACKEND_TEST_CASE_MANIFEST_OUTPUT_INSTRUCTIONS = [
2690
2689
  "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
2690
  "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
2691
  "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.",
2692
+ "Use full BE-<MODULE>-<NNN> caseId strings. Do not invent coverage percentages and do not emit coverageSummary; shell always writes the canonical summary.",
2694
2693
  "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
2694
  ].join("\n\n");
2696
2695
  function buildEmitBackendCaseManifestNode(sources, options) {
@@ -2769,10 +2768,8 @@ function buildBackendTestTraceabilityGateNode(sources, options = {}) {
2769
2768
  };
2770
2769
  }
2771
2770
  function buildReviewBackendCasesNode(sources, options) {
2772
- const phase = options?.phase ?? "first";
2773
- const isFinal = phase === "final";
2774
2771
  return {
2775
- id: options?.id ?? "review-backend-cases-pi",
2772
+ id: "review-backend-cases-pi",
2776
2773
  depends_on: options?.dependsOn ?? [
2777
2774
  "backend-test-case-manifest-shell",
2778
2775
  "backend-test-analysis-contract-shell",
@@ -2783,13 +2780,9 @@ function buildReviewBackendCasesNode(sources, options) {
2783
2780
  writePolicy: "read-only",
2784
2781
  allowedPaths: commonReadOnlyPaths(sources),
2785
2782
  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.",
2783
+ 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
2784
  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.",
2785
+ "Review the generated backend functional test cases under testcase/md/ and the validated Case Manifest v1.",
2793
2786
  "",
2794
2787
  "## Mandatory First Line:",
2795
2788
  "First non-empty line must be exactly: VERDICT: pass or VERDICT: request-revision",
@@ -2818,15 +2811,13 @@ function buildReviewBackendCasesNode(sources, options) {
2818
2811
  "## Verdict Rules:",
2819
2812
  "- All Critical checks pass + Important findings ≤ 2 → VERDICT: pass",
2820
2813
  "- 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/**.",
2814
+ "- Any request-revision verdict ends the current run at the deterministic gate; describe findings clearly for an independent follow-up task.",
2824
2815
  "",
2825
2816
  "## Output After Verdict:",
2826
2817
  "1. Coverage Assessment table (AC → full BE-* case IDs) using manifest + MD",
2827
2818
  "2. Findings list (Critical/Important/Informational)",
2828
2819
  "3. Statistics (total cases, positive/negative/boundary breakdown)",
2829
- isFinal ? "" : "4. Required revisions (only when request-revision)",
2820
+ "4. Required follow-up actions (only when request-revision; no in-run writer)",
2830
2821
  "",
2831
2822
  "## Constraints:",
2832
2823
  "- Read-only: do not modify files",
@@ -2837,69 +2828,6 @@ function buildReviewBackendCasesNode(sources, options) {
2837
2828
  .join("\n\n"),
2838
2829
  };
2839
2830
  }
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
2831
  function buildReviewBackendCasesGateNode(sources) {
2904
2832
  return {
2905
2833
  id: "review-backend-cases-gate-shell",
@@ -3053,21 +2981,18 @@ function buildGenerateBackendPytestNode(sources) {
3053
2981
  "- If a test filename exists, add suffix: test_order.py → test_order_01.py",
3054
2982
  "- Do NOT re-read source documents — use reviewed cases under testcase/md/ and upstream analyze-inputs-pi output only",
3055
2983
  "- 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.",
2984
+ "- 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
2985
  ].join("\n\n"),
3058
2986
  };
3059
2987
  }
3060
2988
  function buildBackendTestSemanticReviewNode(sources, options = {}) {
3061
- const final = options.final ?? false;
3062
2989
  return {
3063
- id: options.id ?? "review-generated-backend-pytest-pi",
2990
+ id: "review-generated-backend-pytest-pi",
3064
2991
  depends_on: options.dependsOn ?? [
3065
2992
  "generate-backend-pytest-pi",
3066
2993
  "backend-test-analysis-contract-shell",
3067
2994
  "backend-test-case-manifest-shell",
3068
- "backend-test-case-manifest-final-shell",
3069
2995
  ],
3070
- dependsPolicy: "all-or-condition-skip",
3071
2996
  role: "reviewer",
3072
2997
  executor: "pi",
3073
2998
  complexity: "MED",
@@ -3076,7 +3001,7 @@ function buildBackendTestSemanticReviewNode(sources, options = {}) {
3076
3001
  forbiddenPaths: commonForbiddenPaths(sources),
3077
3002
  outputContract: "Pure Backend Test Semantic Review v1 JSON: verdict, findings[], summary. No file writes.",
3078
3003
  subtask_prompt: [
3079
- final ? "Final semantic review after the single generated-pytest revision." : "Review generated pytest semantics before the first execution.",
3004
+ "Review generated pytest semantics before the single execution.",
3080
3005
  "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
3006
  "Return exactly one pure JSON object with only verdict, findings, summary; no Markdown fence or surrounding prose.",
3082
3007
  "verdict must be pass or request-revision. Each findings[] item must contain exactly severity, caseId, testFile, testSymbol, contractRefs, issue, requiredChange.",
@@ -3089,98 +3014,6 @@ function buildBackendTestSemanticReviewNode(sources, options = {}) {
3089
3014
  ].join("\n\n"),
3090
3015
  };
3091
3016
  }
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
3017
  function collectBackendTestShellEnvAllowlist(sources) {
3185
3018
  const names = new Set();
3186
3019
  for (const verify of sources.taskConfig.verifyCommands) {
@@ -3228,7 +3061,7 @@ function buildExecuteBackendPytestNode(sources, options = {}) {
3228
3061
  return {
3229
3062
  id: nodeId,
3230
3063
  depends_on: options.dependsOn ?? [
3231
- "backend-test-semantic-final-gate-shell",
3064
+ "backend-test-semantic-gate-shell",
3232
3065
  "backend-test-execution-contract-shell",
3233
3066
  ],
3234
3067
  role: "verifier",
@@ -3254,41 +3087,10 @@ function buildExecuteBackendPytestNode(sources, options = {}) {
3254
3087
  },
3255
3088
  };
3256
3089
  }
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
3090
  function buildClassifyBackendTestResultNode(sources) {
3289
3091
  return {
3290
3092
  id: "classify-backend-test-result-pi",
3291
- depends_on: ["parse-backend-test-result-initial-shell"],
3093
+ depends_on: ["execute-and-parse-backend-pytest-shell"],
3292
3094
  role: "reviewer",
3293
3095
  executor: "pi",
3294
3096
  complexity: "MED",
@@ -3313,155 +3115,6 @@ function buildClassifyBackendTestResultNode(sources) {
3313
3115
  ].join("\n\n"),
3314
3116
  };
3315
3117
  }
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
3118
  function buildTestRetrospectNode(sources) {
3466
3119
  const canWriteReport = taskAllowsBackendTestReportWrite(sources);
3467
3120
  return {
@@ -3567,10 +3220,11 @@ function buildBackendTestHybridDag(sources) {
3567
3220
  const globalConstraints = [
3568
3221
  ...taskConfig.hardConstraints,
3569
3222
  ...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.",
3223
+ "backend-test-dag uses exactly 15 real top-level tasks and executes pytest exactly once.",
3224
+ "Case and semantic request-revision verdicts fail at deterministic gates; no in-run revision or repair writer is authorized.",
3225
+ "Analysis, execution, manifest, semantic review, single-run result, classification, canonical result, retrospective and outcome evidence remain run-owned and fail-closed.",
3572
3226
  "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.",
3227
+ "pytest writers may only create the initially declared testcase assets; production code, config, skip/xfail, swallowed failures and mock substitution are forbidden.",
3574
3228
  ];
3575
3229
  const analyze = buildAnalyzeInputsNode(sources);
3576
3230
  analyze.id = "analyze-and-discover-backend-test-pi";
@@ -3589,64 +3243,49 @@ function buildBackendTestHybridDag(sources) {
3589
3243
  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
3244
  const manifest = buildBackendTestCaseManifestGateNode(sources, { dependsOn: [generateCases.id], fromNodeId: generateCases.id });
3591
3245
  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
3246
  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" };
3247
+ caseGate.depends_on = [reviewCases.id];
3248
+ caseGate.dependsPolicy = undefined;
3249
+ caseGate.outputContract = "Deterministic backend case review gate: exit 0 only when the first and only review emits VERDICT: pass.";
3250
+ caseGate.subtask_prompt = "Block pytest generation when backend case review requests revision; do not authorize an in-run writer.";
3251
+ caseGate.shell.verdictGate = { fromNodeId: reviewCases.id, accept: ["VERDICT: pass"], label: "backend case review", lineMode: "first-verdict-line" };
3604
3252
  const generatePytest = buildGenerateBackendPytestNode(sources);
3605
3253
  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.",
3254
+ const semanticReview = buildBackendTestSemanticReviewNode(sources, { dependsOn: [generatePytest.id, contracts.id, manifest.id] });
3255
+ const semanticMaterialize = {
3256
+ id: "validate-semantic-review-and-traceability-shell", depends_on: [semanticReview.id, manifest.id], role: "verifier", executor: "shell", complexity: "LOW",
3257
+ writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources),
3258
+ 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.",
3259
+ shell: { commands: [], backendTestPipeline: "semantic-initial", cwd: ".", timeoutMs: 60000 },
3260
+ };
3261
+ const semanticGate = {
3262
+ id: "backend-test-semantic-gate-shell", depends_on: [semanticMaterialize.id, semanticReview.id], role: "verifier", executor: "shell", complexity: "LOW",
3263
+ writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources),
3264
+ outputContract: "Pass-only authorization by reading contracts/backend-test-semantic-review.json; only verdict=pass proceeds to the single pytest execution.",
3265
+ 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
3266
  shell: {
3627
3267
  commands: [],
3628
- backendTestPipeline: "semantic-effective",
3268
+ backendTestPipeline: "semantic-initial",
3629
3269
  cwd: ".",
3630
3270
  timeoutMs: 60000,
3631
3271
  },
3632
3272
  };
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";
3273
+ const execute = buildExecuteBackendPytestNode(sources, { id: "execute-and-parse-backend-pytest-shell", dependsOn: [semanticGate.id, contracts.id], reportStem: "backend-test-initial" });
3274
+ execute.shell.backendTestPipeline = "execute-parse-initial";
3635
3275
  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.";
3276
+ classify.depends_on = [execute.id];
3277
+ const context = {
3278
+ id: "materialize-classification-and-result-context-shell", depends_on: [classify.id, manifest.id], role: "verifier", executor: "shell", complexity: "LOW",
3279
+ writePolicy: "read-only", allowedPaths: commonReadOnlyPaths(sources), forbiddenPaths: commonForbiddenPaths(sources),
3280
+ outputContract: "Materialize Classification v1, copy the unique initial Result to canonical contracts/backend-test-result.json, and emit Result + Manifest + Classification context.",
3281
+ subtask_prompt: "Validate classification and materialize canonical single-run result context without repair eligibility or rerun.",
3282
+ shell: { commands: [], backendTestPipeline: "classification-result-context", cwd: ".", timeoutMs: 60000 },
3283
+ };
3646
3284
  const retrospect = buildTestRetrospectNode(sources);
3647
- retrospect.depends_on = [finalize.id];
3285
+ retrospect.depends_on = [context.id];
3286
+ retrospect.subtask_prompt = retrospect.subtask_prompt.replaceAll("select-effective-backend-test-result-shell", context.id);
3648
3287
  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];
3288
+ const tasks = [analyze, contracts, generateCases, manifest, reviewCases, caseGate, generatePytest, semanticReview, semanticMaterialize, semanticGate, execute, classify, context, retrospect, outcome];
3650
3289
  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
3290
  applyDefaultReadOnlyRetryPolicy(spec);
3652
3291
  parseDagSpec(spec);
@@ -422,9 +422,10 @@ export async function executeDagNode(input) {
422
422
  node.structuredArtifactSha256 = createHash("sha256").update(bytes).digest("hex");
423
423
  node.structuredArtifactSchemaId = task.shell.jsonArtifactGate.schemaId;
424
424
  }
425
- else if (task.shell?.backendTestPipeline === "finalize-effective-result") {
426
- // Pipeline materializes contracts/backend-test-result.json without jsonArtifactGate.
427
- // Bind it so Outcome adapters project kind=backend-test-result for Ready Planner.
425
+ else if (task.shell?.backendTestPipeline === "classification-result-context") {
426
+ // The current 15-node single-run pipeline materializes the canonical
427
+ // contracts/backend-test-result.json without jsonArtifactGate. Bind it
428
+ // so Outcome adapters project kind=backend-test-result for Ready Planner.
428
429
  const artifactPath = path.join(runDir, "contracts", "backend-test-result.json");
429
430
  if (existsSync(artifactPath)) {
430
431
  const bytes = await readFile(artifactPath);
@@ -145,12 +145,8 @@ export const dagNodeStatusSchema = z.enum([
145
145
  export const dagBackendTestPipelineSchema = z.enum([
146
146
  "contracts",
147
147
  "semantic-initial",
148
- "semantic-final",
149
- "semantic-effective",
150
148
  "execute-parse-initial",
151
- "classification-eligibility",
152
- "repair-safety-traceability",
153
- "finalize-effective-result",
149
+ "classification-result-context",
154
150
  ]);
155
151
  export const dagShellConfigSchema = z.object({
156
152
  commands: z.array(z.string()).default([]),