@frankzhang2026/opencode-android-orchestrator 1.0.3 → 1.0.5

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 (34) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +8 -8
  3. package/dist/installer/adaptive-templates.d.ts +1 -1
  4. package/dist/installer/adaptive-templates.d.ts.map +1 -1
  5. package/dist/installer/adaptive-templates.js +11 -0
  6. package/dist/installer/adaptive-templates.js.map +1 -1
  7. package/dist/installer/opencode-config.d.ts +3 -3
  8. package/dist/installer/opencode-config.js +1 -1
  9. package/dist/queue/cli.js +1 -1
  10. package/dist/queue/cli.js.map +1 -1
  11. package/dist/queue/queue.d.ts.map +1 -1
  12. package/dist/queue/queue.js +57 -0
  13. package/dist/queue/queue.js.map +1 -1
  14. package/dist/queue/tools.js +1 -1
  15. package/dist/queue/tools.js.map +1 -1
  16. package/docs/MIGRATION.md +8 -8
  17. package/docs/QUEUE.md +35 -7
  18. package/docs/SECURITY.md +1 -1
  19. package/docs/TROUBLESHOOTING.md +8 -7
  20. package/package.json +1 -1
  21. package/templates/.opencode/agents/scheduled-coder.md +9 -0
  22. package/templates/.opencode/agents/scheduled-planner.md +14 -1
  23. package/templates/.opencode/skills/scheduled-quality-coder/SKILL.md +7 -1
  24. package/templates/.opencode/skills/scheduled-quality-orchestrator/SKILL.md +3 -0
  25. package/templates/.opencode/skills/scheduled-quality-reviewer/SKILL.md +4 -0
  26. package/templates/automation/task-contract.schema.json +76 -4
  27. package/templates/automation/tasks/TASK-TEMPLATE.json.example +25 -2
  28. package/templates/scripts/automation/acceptance-report.sh +12 -0
  29. package/templates/scripts/automation/lib.sh +108 -0
  30. package/templates/scripts/automation/record-red.sh +170 -32
  31. package/templates/scripts/automation/show-acceptance-review.sh +7 -0
  32. package/templates/scripts/automation/status.sh +6 -0
  33. package/templates/scripts/automation/validate-contract.sh +46 -2
  34. package/templates/scripts/automation/verify-task.sh +14 -0
@@ -6,51 +6,189 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
6
  source "$SCRIPT_DIR/lib.sh"
7
7
 
8
8
  task_id="${1:-}"
9
- expected="${2:-}"
10
- separator="${3:-}"
11
- filter="${4:-}"
12
-
13
- if [[ -z "$task_id" || -z "$expected" || "$separator" != "--" || -z "$filter" || "$#" -ne 4 ]]; then
14
- printf 'Usage: %s TASK-ID EXPECTED-FAILURE-TEXT -- TEST-FILTER\n' "$0" >&2
15
- exit 2
16
- fi
17
-
9
+ [[ -n "$task_id" ]] || { printf 'Usage: %s TASK-ID [LEGACY-EXPECTED -- LEGACY-FILTER]\n' "$0" >&2; exit 2; }
18
10
  automation_validate_task_id "$task_id"
19
11
  automation_require_queue_execution "$task_id"
20
- automation_validate_test_filter "$filter"
21
- [[ ${#expected} -ge 3 ]] || automation_die "expected failure text is too short"
22
12
  [[ "$(automation_read_state "$task_id")" == "CODING" ]] || automation_die "$task_id is not CODING"
23
13
  "$SCRIPT_DIR/validate-contract.sh" "$task_id"
24
14
 
25
15
  contract="$(automation_contract_path "$task_id")"
26
- target_count="$(jq -er --arg filter "$filter" '[.targetTests[] | select(.filter == $filter)] | length' "$contract")"
27
- [[ "$target_count" -eq 1 ]] || automation_die "test filter must identify exactly one declared contract target: $filter"
28
- gradle_task="$(jq -er --arg filter "$filter" '.targetTests[] | select(.filter == $filter) | .gradleTask' "$contract")"
29
-
16
+ schema_version="$(jq -er '.schemaVersion' "$contract")"
30
17
  evidence_dir="$(automation_evidence_path "$task_id")"
31
18
  mkdir -p "$evidence_dir"
32
- red_log="$evidence_dir/red.log"
33
19
  red_meta="$evidence_dir/red.json"
34
20
  [[ ! -e "$red_meta" ]] || automation_die "RED evidence already exists for $task_id"
35
- started_at="$(automation_now)"
36
21
 
37
- set +e
38
- automation_run_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT" 2>&1 | tee "$red_log"
39
- red_status=${PIPESTATUS[0]}
40
- set -e
22
+ # Preserve the exact V1/V2 behavior for already approved contracts.
23
+ if [[ "$schema_version" != "3" ]]; then
24
+ expected="${2:-}"
25
+ separator="${3:-}"
26
+ filter="${4:-}"
27
+ if [[ -z "$expected" || "$separator" != "--" || -z "$filter" || "$#" -ne 4 ]]; then
28
+ printf 'Usage: %s TASK-ID EXPECTED-FAILURE-TEXT -- TEST-FILTER\n' "$0" >&2
29
+ exit 2
30
+ fi
31
+ automation_validate_test_filter "$filter"
32
+ [[ ${#expected} -ge 3 ]] || automation_die "expected failure text is too short"
33
+ target_count="$(jq -er --arg filter "$filter" '[.targetTests[] | select(.filter == $filter)] | length' "$contract")"
34
+ [[ "$target_count" -eq 1 ]] || automation_die "test filter must identify exactly one declared contract target: $filter"
35
+ gradle_task="$(jq -er --arg filter "$filter" '.targetTests[] | select(.filter == $filter) | .gradleTask' "$contract")"
36
+ red_log="$evidence_dir/red.log"
37
+ started_at="$(automation_now)"
38
+ set +e
39
+ automation_run_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT" 2>&1 | tee "$red_log"
40
+ red_status=${PIPESTATUS[0]}
41
+ set -e
42
+ [[ "$red_status" -ne 0 ]] || automation_die "RED capture failed: focused test passed before implementation"
43
+ rg -F "$expected" "$red_log" >/dev/null || automation_die "RED output does not contain the expected failure text"
44
+ jq -n --arg taskId "$task_id" --arg startedAt "$started_at" --arg finishedAt "$(automation_now)" \
45
+ --arg expectedFailure "$expected" --arg gradleTask "$gradle_task" --arg testFilter "$filter" \
46
+ --argjson exitCode "$red_status" \
47
+ '{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt,
48
+ command: ["./gradlew", $gradleTask, "--tests", $testFilter],
49
+ expectedFailure: $expectedFailure, exitCode: $exitCode}' | automation_record_json "$red_meta"
50
+ automation_info "$task_id legacy RED evidence recorded"
51
+ exit 0
52
+ fi
53
+
54
+ [[ "$#" -eq 1 ]] || { printf 'Schema V3 usage: %s TASK-ID\n' "$0" >&2; exit 2; }
55
+ "$SCRIPT_DIR/scope-gate.sh" "$task_id" >/dev/null
41
56
 
42
- [[ "$red_status" -ne 0 ]] || automation_die "RED capture failed: focused test passed before implementation"
43
- rg -F "$expected" "$red_log" >/dev/null || automation_die "RED output does not contain the expected failure text"
57
+ # RED must be captured before any production change. Planning artifacts are
58
+ # excluded; test sources and fixtures are the only permitted product changes.
59
+ while IFS= read -r changed_path; do
60
+ [[ -n "$changed_path" ]] || continue
61
+ if ! automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$changed_path"; then
62
+ automation_die "RED preflight requires unchanged production code; non-test path changed: $changed_path"
63
+ fi
64
+ done < <(automation_product_changed_paths_at "$task_id" "$AUTOMATION_ROOT")
65
+
66
+ previous_attempt=0
67
+ preflight_meta="$evidence_dir/test-preflight.json"
68
+ if [[ -f "$preflight_meta" ]]; then
69
+ previous_attempt="$(jq -er '.attempt' "$preflight_meta")"
70
+ fi
71
+ attempt=$((previous_attempt + 1))
72
+ max_fixes="$(jq -er '.verification.maxPreparationFixes' "$contract")"
73
+ if [[ "$attempt" -gt $((max_fixes + 1)) ]]; then
74
+ automation_die "test preparation retry budget exhausted; revise the contract or abort the task"
75
+ fi
76
+
77
+ attempt_id="$(printf '%03d' "$attempt")"
78
+ attempt_dir="$evidence_dir/attempts/red-preflight-$attempt_id"
79
+ mkdir -p "$attempt_dir"
80
+ observed_file="$attempt_dir/observed.jsonl"
81
+ : > "$observed_file"
82
+ execution_failures='[]'
83
+ started_at="$(automation_now)"
84
+ target_index=0
85
+ while IFS=$'\t' read -r gradle_task filter; do
86
+ result_file="$attempt_dir/target-$target_index.jsonl"
87
+ log_file="$attempt_dir/target-$target_index.log"
88
+ set +e
89
+ automation_run_classified_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT" "$result_file" "$log_file"
90
+ gradle_status=$?
91
+ set -e
92
+ if [[ "$gradle_status" -ne 0 ]]; then
93
+ execution_failures="$(jq -nc --argjson current "$execution_failures" --argjson target "$target_index" \
94
+ --arg task "$gradle_task" --arg filter "$filter" --argjson exitCode "$gradle_status" \
95
+ '$current + [{target: $target, gradleTask: $task, filter: $filter, exitCode: $exitCode}]')"
96
+ fi
97
+ if [[ -s "$result_file" ]]; then
98
+ while IFS= read -r result_line; do
99
+ if ! jq -e . >/dev/null 2>&1 <<< "$result_line"; then
100
+ execution_failures="$(jq -nc --argjson current "$execution_failures" --argjson target "$target_index" \
101
+ '$current + [{target: $target, reason: "invalid structured test output"}]')"
102
+ continue
103
+ fi
104
+ jq -c --argjson target "$target_index" '. + {target: $target}' <<< "$result_line" >> "$observed_file"
105
+ done < "$result_file"
106
+ else
107
+ execution_failures="$(jq -nc --argjson current "$execution_failures" --argjson target "$target_index" \
108
+ '$current + [{target: $target, reason: "no structured test results"}]')"
109
+ fi
110
+ target_index=$((target_index + 1))
111
+ done < <(jq -r '.targetTests[] | [.gradleTask, .filter] | @tsv' "$contract")
112
+
113
+ contract_sha="$(automation_file_sha256 "$contract")"
114
+ baseline_head="$(jq -er '.head' "$evidence_dir/baseline.json")"
115
+ test_diff_sha="$(automation_test_diff_sha "$task_id" "$AUTOMATION_ROOT")"
116
+ finished_at="$(automation_now)"
44
117
 
45
118
  jq -n \
119
+ --slurpfile contract "$contract" \
120
+ --slurpfile observed "$observed_file" \
46
121
  --arg taskId "$task_id" \
47
122
  --arg startedAt "$started_at" \
48
- --arg finishedAt "$(automation_now)" \
49
- --arg expectedFailure "$expected" \
50
- --arg gradleTask "$gradle_task" \
51
- --arg testFilter "$filter" \
52
- --argjson exitCode "$red_status" \
53
- '{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt, command: ["./gradlew", $gradleTask, "--tests", $testFilter], expectedFailure: $expectedFailure, exitCode: $exitCode}' \
54
- | automation_record_json "$red_meta"
55
-
56
- automation_info "$task_id RED evidence recorded"
123
+ --arg finishedAt "$finished_at" \
124
+ --arg contractSha256 "$contract_sha" \
125
+ --arg baselineHead "$baseline_head" \
126
+ --arg testDiffSha256 "$test_diff_sha" \
127
+ --argjson attempt "$attempt" \
128
+ --argjson executionFailures "$execution_failures" '
129
+ def failure_matches($decl; $actual):
130
+ ($actual.result == "FAILURE") and
131
+ ($actual.exceptionType == $decl.expectedFailure.type) and
132
+ (($decl.expectedFailure.messageIncludes // "") as $fragment |
133
+ ($fragment == "" or (($actual.exceptionMessage // "") | contains($fragment))));
134
+ ($contract[0].verification.cases | map(
135
+ . as $decl |
136
+ [$observed[] | select(.kind == "case" and .target == $decl.test.target and
137
+ .className == $decl.test.className and .name == $decl.test.name)] as $matches |
138
+ (if ($matches | length) == 1 then $matches[0] else null end) as $actual |
139
+ (if ($matches | length) != 1 then false
140
+ elif $decl.before == "pass" then $actual.result == "SUCCESS"
141
+ elif $decl.before == "fail" then failure_matches($decl; $actual)
142
+ elif $actual.result == "SUCCESS" then true
143
+ elif ($decl | has("expectedFailure")) then failure_matches($decl; $actual)
144
+ else false end) as $valid |
145
+ {id: $decl.id, criterion: $decl.criterion, intent: $decl.intent,
146
+ expectedBefore: $decl.before, test: $decl.test, expectedFailure: ($decl.expectedFailure // null),
147
+ matches: ($matches | length), actual: $actual, valid: $valid}
148
+ )) as $cases |
149
+ [$observed[] | select(.kind == "case") as $actual |
150
+ select(any($contract[0].verification.cases[];
151
+ .test.target == $actual.target and .test.className == $actual.className and .test.name == $actual.name) | not)] as $undeclared |
152
+ [$observed[] | select(.kind == "suite")] as $suites |
153
+ ($executionFailures | length == 0 and
154
+ ($suites | length) >= ($contract[0].targetTests | length) and
155
+ ($cases | all(.valid)) and ($undeclared | length == 0) and
156
+ ($cases | any(.intent == "change" and .actual.result == "FAILURE"))) as $valid |
157
+ {taskId: $taskId, attempt: $attempt, startedAt: $startedAt, finishedAt: $finishedAt,
158
+ contractSha256: $contractSha256, baselineHead: $baselineHead,
159
+ testDiffSha256: $testDiffSha256, valid: $valid,
160
+ reasonCode: (if $valid then "VALID_RED"
161
+ elif ($executionFailures | length) > 0 then "EXECUTION_FAILURE"
162
+ elif ($undeclared | length) > 0 then "UNDECLARED_TEST_RESULT"
163
+ else "CASE_EXPECTATION_MISMATCH" end),
164
+ summary: {declared: ($cases | length), valid: ([$cases[] | select(.valid)] | length),
165
+ invalid: ([$cases[] | select(.valid | not)] | length),
166
+ expectedRed: ([$cases[] | select(.intent == "change" and .actual.result == "FAILURE")] | length),
167
+ undeclared: ($undeclared | length)},
168
+ cases: $cases, undeclaredCases: $undeclared, suites: $suites,
169
+ executionFailures: $executionFailures}' | automation_record_json "$preflight_meta"
170
+
171
+ cp "$preflight_meta" "$attempt_dir/evaluation.json"
172
+ if [[ "$(jq -r '.valid' "$preflight_meta")" != "true" ]]; then
173
+ reason="$(jq -r '.reasonCode' "$preflight_meta")"
174
+ invalid="$(jq -r '[.cases[] | select(.valid | not) | .id] | join(", ")' "$preflight_meta")"
175
+ automation_die "RED preflight rejected ($reason); invalid cases: ${invalid:-none}; evidence: $preflight_meta"
176
+ fi
177
+
178
+ jq '{taskId, contractSha256, baselineHead, testDiffSha256,
179
+ cases: [.cases[] | {id, criterion, intent, test, actual}]}' "$preflight_meta" \
180
+ | automation_record_json "$evidence_dir/test-manifest.json"
181
+ preflight_sha="$(automation_file_sha256 "$preflight_meta")"
182
+ manifest_sha="$(automation_file_sha256 "$evidence_dir/test-manifest.json")"
183
+ jq -n --arg taskId "$task_id" --arg startedAt "$started_at" --arg finishedAt "$finished_at" \
184
+ --arg contractSha256 "$contract_sha" --arg baselineHead "$baseline_head" \
185
+ --arg testDiffSha256 "$test_diff_sha" --arg preflightSha256 "$preflight_sha" \
186
+ --arg manifestSha256 "$manifest_sha" --arg attemptPath "attempts/red-preflight-$attempt_id" \
187
+ --argjson attempt "$attempt" \
188
+ '{taskId: $taskId, schemaVersion: 3, startedAt: $startedAt, finishedAt: $finishedAt,
189
+ contractSha256: $contractSha256, baselineHead: $baselineHead,
190
+ testDiffSha256: $testDiffSha256, preflightSha256: $preflightSha256,
191
+ manifestSha256: $manifestSha256, attempt: $attempt, attemptPath: $attemptPath,
192
+ structuredCasesVerified: true, exitCode: 1}' | automation_record_json "$red_meta"
193
+
194
+ automation_info "$task_id structured RED evidence recorded"
@@ -79,6 +79,13 @@ jq -r '.nonGoals[] | "- \(.)"' "$report_file"
79
79
 
80
80
  printf '\n### P1 · 自动证据是否可信\n\n'
81
81
  printf -- '- RED 退出码:`%s`(应为非 0,证明测试先真实失败)\n' "$(jq -r '.evidence.redExitCode' "$report_file")"
82
+ if [[ "$(jq -r '.evidence.structuredRed != null' "$report_file")" == "true" ]]; then
83
+ printf -- '- 结构化 RED:声明 `%s` 个,用例匹配 `%s` 个,额外用例 `%s` 个\n' \
84
+ "$(jq -r '.evidence.structuredRed.summary.declared' "$report_file")" \
85
+ "$(jq -r '.evidence.structuredRed.summary.valid' "$report_file")" \
86
+ "$(jq -r '.evidence.structuredRed.summary.undeclared' "$report_file")"
87
+ jq -r '.evidence.structuredRed.cases[] | " - `\(.id)` · \(.intent) · 修改前应 \(.expectedBefore) · 已核验 \(.valid)"' "$report_file"
88
+ fi
82
89
  printf -- '- 质量门:`%s`,第 %s 个编码周期内共运行 %s 次\n' \
83
90
  "$(jq -r '.evidence.qualityGate' "$report_file")" \
84
91
  "$(jq -r '.evidence.codingCycle' "$report_file")" \
@@ -21,6 +21,8 @@ workspace_json="$(if [[ -f "$workspace_file" ]]; then jq -c . "$workspace_file";
21
21
  origin_json="$(if [[ -f "$origin_file" ]]; then jq -c . "$origin_file"; else printf 'null'; fi)"
22
22
  baseline_json="$(if [[ -f "$evidence_dir/baseline.json" ]]; then jq -c . "$evidence_dir/baseline.json"; else printf 'null'; fi)"
23
23
  red_json="$(if [[ -f "$evidence_dir/red.json" ]]; then jq -c . "$evidence_dir/red.json"; else printf 'null'; fi)"
24
+ preflight_json="$(if [[ -f "$evidence_dir/test-preflight.json" ]]; then jq -c . "$evidence_dir/test-preflight.json"; else printf 'null'; fi)"
25
+ manifest_json="$(if [[ -f "$evidence_dir/test-manifest.json" ]]; then jq -c . "$evidence_dir/test-manifest.json"; else printf 'null'; fi)"
24
26
  ready_json="$(if [[ -f "$evidence_dir/ready.json" ]]; then jq -c . "$evidence_dir/ready.json"; else printf 'null'; fi)"
25
27
  review_json="$(if [[ -f "$evidence_dir/review.json" ]]; then jq -c . "$evidence_dir/review.json"; else printf 'null'; fi)"
26
28
  gate_json=null
@@ -93,6 +95,8 @@ jq -n \
93
95
  --argjson origin "$origin_json" \
94
96
  --argjson baseline "$baseline_json" \
95
97
  --argjson red "$red_json" \
98
+ --argjson testPreflight "$preflight_json" \
99
+ --argjson testManifest "$manifest_json" \
96
100
  --argjson ready "$ready_json" \
97
101
  --argjson gate "$gate_json" \
98
102
  --argjson review "$review_json" \
@@ -118,6 +122,8 @@ jq -n \
118
122
  directory: $evidence,
119
123
  baseline: $baseline,
120
124
  red: $red,
125
+ testPreflight: $testPreflight,
126
+ testManifest: $testManifest,
121
127
  ready: $ready,
122
128
  latestGate: $gate,
123
129
  review: $review,
@@ -27,7 +27,7 @@ fi
27
27
  jq -e . "$contract" >/dev/null || automation_die "contract is not valid JSON: $contract"
28
28
 
29
29
  jq -e '
30
- (.schemaVersion == 1 or .schemaVersion == 2) and
30
+ (.schemaVersion == 1 or .schemaVersion == 2 or .schemaVersion == 3) and
31
31
  (.id | type == "string" and test("^TASK-[A-Z0-9-]+$")) and
32
32
  (.title | type == "string" and length > 0) and
33
33
  .designApproved == true and
@@ -53,7 +53,51 @@ jq -e '
53
53
  ([.targetTests[].filter] | length == (unique | length)) and
54
54
  (.deviceTestsRequired | type == "boolean") and
55
55
  (.testPolicy == "required" or .testPolicy == "not-required") and
56
- (if .testPolicy == "not-required" then (.testPolicyReason | type == "string" and length >= 20) else true end)
56
+ (if .testPolicy == "not-required" then (.testPolicyReason | type == "string" and length >= 20) else true end) and
57
+ (if .schemaVersion == 3 then
58
+ (.acceptanceCriteria | length) as $criterionCount |
59
+ (.targetTests | length) as $targetCount |
60
+ (.verification | type == "object" and
61
+ keys == ["cases", "maxPreparationFixes", "version"] and
62
+ .version == 1 and
63
+ (.maxPreparationFixes | type == "number" and floor == . and . >= 0 and . <= 1) and
64
+ (.cases | type == "array" and length > 0) and
65
+ ([.cases[].id] | length == (unique | length)) and
66
+ ([.cases[] | [.test.target, .test.className, .test.name] | @json] | length == (unique | length)) and
67
+ (any(.cases[]; .intent == "change")) and
68
+ all(.cases[];
69
+ type == "object" and
70
+ ([keys[]] - ["id", "criterion", "intent", "before", "after", "source", "test", "expectedFailure"] | length == 0) and
71
+ (.id | type == "string" and test("^[A-Z][A-Z0-9-]{2,63}$")) and
72
+ (.criterion | type == "number" and floor == . and . >= 1 and . <= $criterionCount) and
73
+ .after == "pass" and
74
+ (.test | type == "object" and keys == ["className", "name", "target"] and
75
+ (.target | type == "number" and floor == . and . >= 0 and . < $targetCount) and
76
+ (.className | type == "string" and length > 0) and
77
+ (.name | type == "string" and length > 0)) and
78
+ (if .intent == "preserve" then
79
+ .before == "pass" and
80
+ (.source == "existingTest" or .source == "baselineCapture" or .source == "measuredFact") and
81
+ (has("expectedFailure") | not)
82
+ elif .intent == "change" then
83
+ .before == "fail" and .source == "userRequirement" and
84
+ (.expectedFailure | type == "object" and
85
+ ([keys[]] - ["type", "messageIncludes", "origin"] | length == 0) and
86
+ (.type | type == "string" and length > 0) and
87
+ (.origin | type == "string" and length >= 12) and
88
+ (if has("messageIncludes") then (.messageIncludes | type == "string" and length >= 3) else true end))
89
+ elif .intent == "observe" then
90
+ .before == "observe" and
91
+ (.source == "userRequirement" or .source == "existingTest" or .source == "baselineCapture" or .source == "measuredFact") and
92
+ (if has("expectedFailure") then
93
+ (.expectedFailure | type == "object" and
94
+ ([keys[]] - ["type", "messageIncludes", "origin"] | length == 0) and
95
+ (.type | type == "string" and length > 0) and
96
+ (.origin | type == "string" and length >= 12) and
97
+ (if has("messageIncludes") then (.messageIncludes | type == "string" and length >= 3) else true end))
98
+ else true end)
99
+ else false end)))
100
+ else true end)
57
101
  ' "$contract" >/dev/null || automation_die "contract is missing required fields or violates limits"
58
102
 
59
103
  task_id="$(jq -r '.id' "$contract")"
@@ -15,6 +15,20 @@ evidence_dir="$(automation_evidence_path "$task_id")"
15
15
  [[ -f "$evidence_dir/baseline.json" ]] || { automation_die "baseline evidence is missing"; exit 41; }
16
16
  [[ -f "$evidence_dir/red.json" ]] || { automation_die "RED evidence is missing"; exit 41; }
17
17
 
18
+ contract_schema="$(jq -er '.schemaVersion' "$contract")"
19
+ if [[ "$contract_schema" == "3" ]]; then
20
+ preflight_meta="$evidence_dir/test-preflight.json"
21
+ manifest_meta="$evidence_dir/test-manifest.json"
22
+ red_meta="$evidence_dir/red.json"
23
+ [[ -f "$preflight_meta" && -f "$manifest_meta" ]] || { automation_die "structured RED evidence is incomplete"; exit 41; }
24
+ [[ "$(jq -er '.valid' "$preflight_meta")" == "true" ]] || { automation_die "structured RED preflight was not valid"; exit 41; }
25
+ [[ "$(jq -er '.contractSha256' "$red_meta")" == "$(automation_file_sha256 "$contract")" ]] || { automation_die "RED contract binding changed"; exit 41; }
26
+ [[ "$(jq -er '.baselineHead' "$red_meta")" == "$(jq -er '.head' "$evidence_dir/baseline.json")" ]] || { automation_die "RED baseline binding changed"; exit 41; }
27
+ [[ "$(jq -er '.testDiffSha256' "$red_meta")" == "$(automation_test_diff_sha "$task_id" "$AUTOMATION_ROOT")" ]] || { automation_die "tests changed after structured RED"; exit 41; }
28
+ [[ "$(jq -er '.preflightSha256' "$red_meta")" == "$(automation_file_sha256 "$preflight_meta")" ]] || { automation_die "RED preflight evidence changed"; exit 41; }
29
+ [[ "$(jq -er '.manifestSha256' "$red_meta")" == "$(automation_file_sha256 "$manifest_meta")" ]] || { automation_die "RED test manifest changed"; exit 41; }
30
+ fi
31
+
18
32
  "$SCRIPT_DIR/scope-gate.sh" "$task_id"
19
33
 
20
34
  automation_run_configured_unit_tests "$contract" "$AUTOMATION_ROOT"