@frankzhang2026/opencode-android-orchestrator 1.0.4 → 1.1.0
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.
- package/CHANGELOG.md +78 -0
- package/README.md +79 -18
- package/dist/config/queue-policy.d.ts.map +1 -1
- package/dist/config/queue-policy.js +1 -2
- package/dist/config/queue-policy.js.map +1 -1
- package/dist/doctor/index.d.ts.map +1 -1
- package/dist/doctor/index.js +14 -3
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/installation.d.ts +1 -1
- package/dist/doctor/installation.d.ts.map +1 -1
- package/dist/doctor/installation.js +10 -1
- package/dist/doctor/installation.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/installer/adaptive-templates.d.ts +4 -1
- package/dist/installer/adaptive-templates.d.ts.map +1 -1
- package/dist/installer/adaptive-templates.js +49 -13
- package/dist/installer/adaptive-templates.js.map +1 -1
- package/dist/installer/android-project.d.ts +3 -1
- package/dist/installer/android-project.d.ts.map +1 -1
- package/dist/installer/android-project.js.map +1 -1
- package/dist/installer/capabilities-script.d.ts +2 -0
- package/dist/installer/capabilities-script.d.ts.map +1 -0
- package/dist/installer/capabilities-script.js +77 -0
- package/dist/installer/capabilities-script.js.map +1 -0
- package/dist/installer/gradle-verification.d.ts.map +1 -1
- package/dist/installer/gradle-verification.js +13 -6
- package/dist/installer/gradle-verification.js.map +1 -1
- package/dist/installer/init.d.ts.map +1 -1
- package/dist/installer/init.js +2 -1
- package/dist/installer/init.js.map +1 -1
- package/dist/installer/opencode-config.d.ts +3 -3
- package/dist/installer/opencode-config.js +1 -1
- package/dist/installer/project-capabilities.d.ts +35 -0
- package/dist/installer/project-capabilities.d.ts.map +1 -0
- package/dist/installer/project-capabilities.js +82 -0
- package/dist/installer/project-capabilities.js.map +1 -0
- package/dist/installer/upgrade.d.ts.map +1 -1
- package/dist/installer/upgrade.js +9 -1
- package/dist/installer/upgrade.js.map +1 -1
- package/dist/queue/cli.d.ts.map +1 -1
- package/dist/queue/cli.js +32 -5
- package/dist/queue/cli.js.map +1 -1
- package/dist/queue/executor.d.ts +1 -0
- package/dist/queue/executor.d.ts.map +1 -1
- package/dist/queue/executor.js +155 -11
- package/dist/queue/executor.js.map +1 -1
- package/dist/queue/process-ownership.d.ts +12 -0
- package/dist/queue/process-ownership.d.ts.map +1 -0
- package/dist/queue/process-ownership.js +58 -0
- package/dist/queue/process-ownership.js.map +1 -0
- package/dist/queue/queue.d.ts +25 -1
- package/dist/queue/queue.d.ts.map +1 -1
- package/dist/queue/queue.js +152 -11
- package/dist/queue/queue.js.map +1 -1
- package/dist/queue/service.d.ts.map +1 -1
- package/dist/queue/service.js +19 -2
- package/dist/queue/service.js.map +1 -1
- package/dist/queue/storage.d.ts +3 -1
- package/dist/queue/storage.d.ts.map +1 -1
- package/dist/queue/storage.js +25 -15
- package/dist/queue/storage.js.map +1 -1
- package/dist/queue/supervision.d.ts +38 -0
- package/dist/queue/supervision.d.ts.map +1 -0
- package/dist/queue/supervision.js +247 -0
- package/dist/queue/supervision.js.map +1 -0
- package/dist/queue/tools.d.ts.map +1 -1
- package/dist/queue/tools.js +4 -2
- package/dist/queue/tools.js.map +1 -1
- package/docs/MIGRATION.md +110 -9
- package/docs/QUEUE.md +360 -6
- package/docs/SECURITY.md +5 -2
- package/docs/TROUBLESHOOTING.md +82 -8
- package/package.json +1 -1
- package/templates/.opencode/agents/scheduled-coder.md +22 -0
- package/templates/.opencode/agents/scheduled-planner.md +73 -2
- package/templates/.opencode/skills/scheduled-quality-coder/SKILL.md +34 -2
- package/templates/.opencode/skills/scheduled-quality-orchestrator/SKILL.md +65 -4
- package/templates/.opencode/skills/scheduled-quality-reviewer/SKILL.md +22 -0
- package/templates/AGENTS.md.fragment +10 -4
- package/templates/README.md +3 -1
- package/templates/automation/config.schema.json +212 -22
- package/templates/automation/task-contract.schema.json +617 -21
- package/templates/automation/tasks/TASK-TEMPLATE.json.example +31 -3
- package/templates/automation/verification/collect.init.gradle +98 -0
- package/templates/automation/verification/contract.cjs +93 -0
- package/templates/automation/verification/inventory.cjs +433 -0
- package/templates/automation/verification/project.cjs +172 -0
- package/templates/automation/verification/recovery.cjs +426 -0
- package/templates/scripts/automation/acceptance-report.sh +29 -0
- package/templates/scripts/automation/claim-task.sh +35 -2
- package/templates/scripts/automation/integration-scope-gate.sh +3 -3
- package/templates/scripts/automation/lib.sh +167 -17
- package/templates/scripts/automation/orchestrate-task.sh +7 -1
- package/templates/scripts/automation/preflight.sh +1 -2
- package/templates/scripts/automation/quality-gate.sh +10 -1
- package/templates/scripts/automation/record-red.sh +182 -32
- package/templates/scripts/automation/resume-task.sh +3 -0
- package/templates/scripts/automation/scope-gate.sh +4 -4
- package/templates/scripts/automation/show-acceptance-review.sh +8 -0
- package/templates/scripts/automation/status.sh +38 -0
- package/templates/scripts/automation/submit-review.sh +10 -1
- package/templates/scripts/automation/validate-contract.sh +56 -3
- package/templates/scripts/automation/verify-integration.sh +3 -0
- package/templates/scripts/automation/verify-task.sh +17 -0
|
@@ -109,14 +109,14 @@ automation_validate_config() {
|
|
|
109
109
|
def gradle_task:
|
|
110
110
|
type == "string" and
|
|
111
111
|
test("^(?:[A-Za-z][A-Za-z0-9_.-]*|(?::[A-Za-z0-9_.-]+)+)$");
|
|
112
|
-
def
|
|
112
|
+
def optional_gradle_task_list:
|
|
113
113
|
type == "array" and
|
|
114
|
-
length > 0 and
|
|
115
114
|
length == (unique | length) and
|
|
116
115
|
all(.[]; gradle_task);
|
|
116
|
+
def gradle_task_list:
|
|
117
|
+
optional_gradle_task_list and length > 0;
|
|
117
118
|
(.schemaVersion == 5 or .schemaVersion == 6) and
|
|
118
119
|
((.commitPolicy // "humanApproval") == "humanApproval" or .commitPolicy == "autoCommit") and
|
|
119
|
-
(.workspaceStrategy != "isolatedWorktree" or (.commitPolicy // "humanApproval") == "humanApproval") and
|
|
120
120
|
(.enabled | type == "boolean") and
|
|
121
121
|
(.mode == "shadow" or .mode == "orchestrated") and
|
|
122
122
|
(.workspaceStrategy == "inPlaceExclusive" or .workspaceStrategy == "isolatedWorktree") and
|
|
@@ -149,8 +149,9 @@ automation_validate_config() {
|
|
|
149
149
|
($verification.fullUnitTestTasks | gradle_task_list) and
|
|
150
150
|
($verification.focusedTestTasks | gradle_task_list) and
|
|
151
151
|
($verification.assembleTasks | gradle_task_list) and
|
|
152
|
-
($verification.lintTasks |
|
|
153
|
-
($verification.deviceTestTasks |
|
|
152
|
+
($verification.lintTasks | optional_gradle_task_list) and
|
|
153
|
+
($verification.deviceTestTasks | optional_gradle_task_list)) and
|
|
154
|
+
((.lintEnabled | not) or (.gradleVerification.lintTasks | length > 0)) and
|
|
154
155
|
(.androidProject as $project |
|
|
155
156
|
($project | type == "object") and
|
|
156
157
|
($project.name | type == "string" and length > 0) and
|
|
@@ -163,16 +164,19 @@ automation_validate_config() {
|
|
|
163
164
|
(.directory | repository_path) and
|
|
164
165
|
(.buildFile | repository_path) and
|
|
165
166
|
(.dsl == "kotlin" or .dsl == "groovy") and
|
|
166
|
-
(.type == "application" or .type == "library" or .type == "dynamic-feature" or .type == "test" or .type == "asset-pack") and
|
|
167
|
+
(.type == "application" or .type == "library" or .type == "dynamic-feature" or .type == "test" or .type == "asset-pack" or ($project.capabilities.version == 1 and .type == "jvm-library")) and
|
|
167
168
|
(.namespace == null or (.namespace | type == "string" and length > 0)) and
|
|
168
169
|
(.applicationId == null or (.applicationId | type == "string" and length > 0)))) and
|
|
169
170
|
any($project.modules[]; .gradlePath == $project.primaryModule) and
|
|
170
171
|
($project.productionPaths | type == "array" and length > 0 and all(.[];
|
|
171
|
-
repository_path and test("(^|/)src/main/\\*\\*$"))) and
|
|
172
|
+
repository_path and ($project.capabilities.version == 1 or test("(^|/)src/main/\\*\\*$")))) and
|
|
172
173
|
($project.testPaths | type == "array" and length > 0 and all(.[];
|
|
173
|
-
repository_path and test("(^|/)src/(?:test|androidTest)/\\*\\*$")))) and
|
|
174
|
+
repository_path and ($project.capabilities.version == 1 or test("(^|/)src/(?:test|androidTest)/\\*\\*$"))))) and
|
|
174
175
|
(.protectedPaths | type == "array" and length > 0 and all(.[]; repository_path))
|
|
175
176
|
' "$AUTOMATION_CONFIG" >/dev/null || automation_die "automation/config.json is invalid"
|
|
177
|
+
if jq -e '.androidProject | has("capabilities")' "$AUTOMATION_CONFIG" >/dev/null; then
|
|
178
|
+
node "$AUTOMATION_ROOT/automation/verification/project.cjs" "$AUTOMATION_CONFIG" "$AUTOMATION_ROOT" || automation_die "Android capability snapshot is invalid"
|
|
179
|
+
fi
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
automation_require_orchestrated() {
|
|
@@ -250,7 +254,7 @@ automation_require_queue_execution() {
|
|
|
250
254
|
return 1
|
|
251
255
|
}
|
|
252
256
|
if ! worker_pid="$(jq -er --arg key "$queue_key" --arg run "$AUTOMATION_QUEUE_RUN_ID" \
|
|
253
|
-
'select(.active.key == $key and .active.id == $run) | .active.worker.pid' \
|
|
257
|
+
'select(.active.key == $key and .active.id == $run and (.active.supervision == null or .active.supervision.state == "RUNNING")) | .active.worker.pid' \
|
|
254
258
|
"$AUTOMATION_RUNTIME_ROOT/inbox/queue.json")"; then
|
|
255
259
|
automation_die "current process does not own this task queue execution"
|
|
256
260
|
return 1
|
|
@@ -414,14 +418,26 @@ automation_gradle_group_command_json() {
|
|
|
414
418
|
automation_validate_config || return 1
|
|
415
419
|
jq -ce \
|
|
416
420
|
--arg group "$group" \
|
|
417
|
-
|
|
421
|
+
--arg supervised "${AUTOMATION_WORKER_TOKEN:+yes}" \
|
|
422
|
+
'["./gradlew"] + .gradleVerification[$group] + (if $supervised == "yes" then ["--no-daemon"] else [] end)' \
|
|
418
423
|
"$AUTOMATION_CONFIG"
|
|
419
424
|
}
|
|
420
425
|
|
|
426
|
+
# Keep Git/scope/evidence operations at the repository root. Only Gradle changes
|
|
427
|
+
# cwd, resolving the persisted relative build selection in the current worktree.
|
|
428
|
+
automation_gradle_build_root() {
|
|
429
|
+
local root="${1:-$AUTOMATION_ROOT}"
|
|
430
|
+
if jq -e '.androidProject.capabilities != null' "$AUTOMATION_CONFIG" >/dev/null; then
|
|
431
|
+
node "$AUTOMATION_ROOT/automation/verification/project.cjs" "$AUTOMATION_CONFIG" "$root" --build-root
|
|
432
|
+
else
|
|
433
|
+
printf '%s\n' "$root"
|
|
434
|
+
fi
|
|
435
|
+
}
|
|
436
|
+
|
|
421
437
|
automation_run_gradle_group() {
|
|
422
438
|
local group="$1"
|
|
423
439
|
local root="${2:-$AUTOMATION_ROOT}"
|
|
424
|
-
local task
|
|
440
|
+
local task build_root
|
|
425
441
|
local -a tasks=()
|
|
426
442
|
|
|
427
443
|
automation_validate_gradle_group "$group" || return 1
|
|
@@ -440,8 +456,10 @@ automation_run_gradle_group() {
|
|
|
440
456
|
return $?
|
|
441
457
|
fi
|
|
442
458
|
(
|
|
443
|
-
|
|
444
|
-
|
|
459
|
+
local build_root
|
|
460
|
+
build_root="$(automation_gradle_build_root "$root")" || exit 1
|
|
461
|
+
cd "$build_root" || exit 1
|
|
462
|
+
./gradlew "${tasks[@]}" ${AUTOMATION_WORKER_TOKEN:+--no-daemon}
|
|
445
463
|
)
|
|
446
464
|
}
|
|
447
465
|
|
|
@@ -451,7 +469,7 @@ automation_run_gradle_group() {
|
|
|
451
469
|
automation_run_fresh_unit_tests() {
|
|
452
470
|
local root="$1"
|
|
453
471
|
shift
|
|
454
|
-
local init_file log_file result elapsed start
|
|
472
|
+
local init_file log_file result elapsed start build_root
|
|
455
473
|
mkdir -p "$AUTOMATION_RUNTIME_ROOT/gradle"
|
|
456
474
|
init_file="$(mktemp "$AUTOMATION_RUNTIME_ROOT/gradle/fresh-tests.XXXXXX")"
|
|
457
475
|
log_file="${init_file}.log"
|
|
@@ -481,7 +499,7 @@ gradle.taskGraph.afterTask { task, state ->
|
|
|
481
499
|
GRADLE
|
|
482
500
|
start="$(date +%s)"
|
|
483
501
|
set +e
|
|
484
|
-
(
|
|
502
|
+
(build_root="$(automation_gradle_build_root "$root")" && cd "$build_root" && ./gradlew "$@" ${AUTOMATION_WORKER_TOKEN:+--no-daemon} --no-configuration-cache --console=plain --init-script "$init_file") 2>&1 | tee "$log_file"
|
|
485
503
|
result=${PIPESTATUS[0]}
|
|
486
504
|
set -e
|
|
487
505
|
elapsed=$(( $(date +%s) - start ))
|
|
@@ -522,10 +540,14 @@ automation_run_configured_unit_tests() {
|
|
|
522
540
|
return 0
|
|
523
541
|
fi
|
|
524
542
|
|
|
543
|
+
if [[ "$(jq -r '.schemaVersion' "$contract")" -ge "4" ]]; then
|
|
544
|
+
automation_run_inventory green "$(jq -er '.id' "$contract")" "$root" || return 1
|
|
545
|
+
else
|
|
525
546
|
while IFS=$'\t' read -r gradle_task filter; do
|
|
526
547
|
automation_info "running ${context}focused test ($gradle_task): $filter"
|
|
527
548
|
automation_run_focused_test "$gradle_task" "$filter" "$root"
|
|
528
549
|
done < <(jq -r '.targetTests[] | [.gradleTask, .filter] | @tsv' "$contract")
|
|
550
|
+
fi
|
|
529
551
|
|
|
530
552
|
automation_info "running ${context}full unit tests"
|
|
531
553
|
automation_run_gradle_group "fullUnitTestTasks" "$root"
|
|
@@ -561,11 +583,139 @@ automation_run_focused_test() {
|
|
|
561
583
|
}
|
|
562
584
|
|
|
563
585
|
(
|
|
564
|
-
|
|
565
|
-
|
|
586
|
+
local build_root
|
|
587
|
+
build_root="$(automation_gradle_build_root "$root")" || exit 1
|
|
588
|
+
cd "$build_root" || exit 1
|
|
589
|
+
./gradlew "$task" --tests "$filter" ${AUTOMATION_WORKER_TOKEN:+--no-daemon}
|
|
566
590
|
)
|
|
567
591
|
}
|
|
568
592
|
|
|
593
|
+
# Run one focused target with fresh Test execution and machine-readable case
|
|
594
|
+
# results. Test assertion failures are collected instead of failing Gradle so
|
|
595
|
+
# the caller can distinguish approved RED from build and fixture failures.
|
|
596
|
+
automation_run_classified_focused_test() {
|
|
597
|
+
local task="$1"
|
|
598
|
+
local filter="$2"
|
|
599
|
+
local root="$3"
|
|
600
|
+
local result_file="$4"
|
|
601
|
+
local log_file="$5"
|
|
602
|
+
local init_file status build_root
|
|
603
|
+
|
|
604
|
+
automation_validate_config || return 1
|
|
605
|
+
automation_validate_gradle_task "$task" || return 1
|
|
606
|
+
automation_validate_test_filter "$filter" || return 1
|
|
607
|
+
jq -e --arg task "$task" \
|
|
608
|
+
'.gradleVerification.focusedTestTasks | index($task) != null' \
|
|
609
|
+
"$AUTOMATION_CONFIG" >/dev/null || {
|
|
610
|
+
automation_die "focused Gradle task is not allowed by automation/config.json: $task"
|
|
611
|
+
return 1
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
mkdir -p "$AUTOMATION_RUNTIME_ROOT/gradle" "$(dirname "$result_file")" "$(dirname "$log_file")"
|
|
615
|
+
init_file="$(mktemp "$AUTOMATION_RUNTIME_ROOT/gradle/classified-tests.XXXXXX")"
|
|
616
|
+
: > "$result_file"
|
|
617
|
+
cat > "$init_file" <<'GRADLE'
|
|
618
|
+
import groovy.json.JsonOutput
|
|
619
|
+
import org.gradle.api.tasks.testing.Test
|
|
620
|
+
import org.gradle.api.tasks.testing.TestDescriptor
|
|
621
|
+
import org.gradle.api.tasks.testing.TestListener
|
|
622
|
+
import org.gradle.api.tasks.testing.TestResult
|
|
623
|
+
|
|
624
|
+
def outputPath = System.getProperty('orchestrator.caseResultFile')
|
|
625
|
+
if (outputPath == null || outputPath.isEmpty()) {
|
|
626
|
+
throw new GradleException('orchestrator.caseResultFile is required')
|
|
627
|
+
}
|
|
628
|
+
def outputFile = new File(outputPath)
|
|
629
|
+
def appendResult = { Map value ->
|
|
630
|
+
synchronized (gradle) {
|
|
631
|
+
outputFile << JsonOutput.toJson(value) << System.lineSeparator()
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
gradle.allprojects { project ->
|
|
636
|
+
project.tasks.withType(Test).configureEach { testTask ->
|
|
637
|
+
outputs.upToDateWhen { false }
|
|
638
|
+
outputs.doNotCacheIf('Orchestrator requires fresh classified test execution') { true }
|
|
639
|
+
ignoreFailures = true
|
|
640
|
+
failFast = false
|
|
641
|
+
if (testTask.hasProperty('dryRun')) testTask.dryRun = false
|
|
642
|
+
addTestListener(new TestListener() {
|
|
643
|
+
void beforeSuite(TestDescriptor descriptor) {}
|
|
644
|
+
void beforeTest(TestDescriptor descriptor) {}
|
|
645
|
+
void afterTest(TestDescriptor descriptor, TestResult result) {
|
|
646
|
+
def failure = result.exceptions == null || result.exceptions.isEmpty() ? null : result.exceptions[0]
|
|
647
|
+
appendResult([
|
|
648
|
+
kind: 'case', taskPath: testTask.path,
|
|
649
|
+
className: descriptor.className ?: '', name: descriptor.name ?: '',
|
|
650
|
+
result: result.resultType.toString(),
|
|
651
|
+
exceptionType: failure == null ? null : failure.class.name,
|
|
652
|
+
exceptionMessage: failure == null ? null : (failure.message ?: '')
|
|
653
|
+
])
|
|
654
|
+
}
|
|
655
|
+
void afterSuite(TestDescriptor descriptor, TestResult result) {
|
|
656
|
+
if (descriptor.parent == null) {
|
|
657
|
+
appendResult([
|
|
658
|
+
kind: 'suite', taskPath: testTask.path,
|
|
659
|
+
tests: result.testCount, failures: result.failedTestCount,
|
|
660
|
+
skipped: result.skippedTestCount,
|
|
661
|
+
result: result.resultType.toString()
|
|
662
|
+
])
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
GRADLE
|
|
669
|
+
|
|
670
|
+
set +e
|
|
671
|
+
(build_root="$(automation_gradle_build_root "$root")" && cd "$build_root" && ./gradlew "$task" --tests "$filter" ${AUTOMATION_WORKER_TOKEN:+--no-daemon} \
|
|
672
|
+
--no-configuration-cache --console=plain --init-script "$init_file" \
|
|
673
|
+
"-Dorchestrator.caseResultFile=$result_file") 2>&1 | tee "$log_file"
|
|
674
|
+
status=${PIPESTATUS[0]}
|
|
675
|
+
set -e
|
|
676
|
+
rm -f "$init_file"
|
|
677
|
+
return "$status"
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
automation_test_diff_sha() {
|
|
681
|
+
local task_id="$1"
|
|
682
|
+
local root="${2:-$AUTOMATION_ROOT}"
|
|
683
|
+
local path tracked=0
|
|
684
|
+
{
|
|
685
|
+
while IFS= read -r path; do
|
|
686
|
+
[[ -n "$path" ]] || continue
|
|
687
|
+
if automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$path"; then
|
|
688
|
+
tracked=1
|
|
689
|
+
if git -C "$root" ls-files --error-unmatch -- "$path" >/dev/null 2>&1; then
|
|
690
|
+
git -C "$root" diff --binary --no-renames HEAD -- "$path"
|
|
691
|
+
else
|
|
692
|
+
printf 'UNTRACKED %s\0' "$path"
|
|
693
|
+
git -C "$root" hash-object -- "$path"
|
|
694
|
+
fi
|
|
695
|
+
fi
|
|
696
|
+
done < <(automation_product_changed_paths_at "$task_id" "$root")
|
|
697
|
+
[[ "$tracked" == "1" ]] || printf 'NO-TEST-CHANGES'
|
|
698
|
+
} | shasum -a 256 | awk '{print $1}'
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
automation_run_inventory() {
|
|
702
|
+
local phase="$1" task_id="$2" root="${3:-$AUTOMATION_ROOT}"
|
|
703
|
+
node "$AUTOMATION_ROOT/automation/verification/inventory.cjs" "$phase" \
|
|
704
|
+
"$(automation_contract_path "$task_id")" "$AUTOMATION_CONFIG" "$root" "$(automation_evidence_path "$task_id")"
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
# Discovery only classifies evidence inputs. Scope gates still enforce the
|
|
708
|
+
# approved paths, forbidden paths, protected files and the file-count limit.
|
|
709
|
+
automation_is_task_test_path() {
|
|
710
|
+
local task_id="$1" path="$2"
|
|
711
|
+
if [[ "$(jq -r '.schemaVersion' "$(automation_contract_path "$task_id")")" -ge "4" ]]; then
|
|
712
|
+
node "$AUTOMATION_ROOT/automation/verification/inventory.cjs" is-test \
|
|
713
|
+
"$(automation_evidence_path "$task_id")/baseline-inventory.json" "$AUTOMATION_CONFIG" "$AUTOMATION_ROOT" "$path"
|
|
714
|
+
else
|
|
715
|
+
automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$path"
|
|
716
|
+
fi
|
|
717
|
+
}
|
|
718
|
+
|
|
569
719
|
automation_require_approval() {
|
|
570
720
|
local kind="$1"
|
|
571
721
|
local supplied="$2"
|
|
@@ -63,7 +63,13 @@ run_agent() {
|
|
|
63
63
|
return "$exit_code"
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
# Each review correction consumes four transitions: resume, code, begin review,
|
|
67
|
+
# and review. Include the initial three transitions, budget-exhaustion
|
|
68
|
+
# transition, and the final state check.
|
|
69
|
+
# The bound must agree with the approved number of review correction cycles.
|
|
70
|
+
max_review_cycles="$(automation_config_value '.maxReviewCycles')"
|
|
71
|
+
max_steps=$((4 * (max_review_cycles + 1) + 1))
|
|
72
|
+
for ((_step = 0; _step < max_steps; _step++)); do
|
|
67
73
|
if [[ -n "${AUTOMATION_QUEUE_RUN_ID:-}" ]] && jq -e --arg id "$task_id" \
|
|
68
74
|
'any(.items[]; .taskId == $id and .request.kind == "abort")' \
|
|
69
75
|
"$AUTOMATION_RUNTIME_ROOT/inbox/queue.json" >/dev/null; then
|
|
@@ -34,9 +34,8 @@ automation_require_command shasum
|
|
|
34
34
|
if [[ "${AUTOMATION_TEST_MODE:-0}" != "1" ]]; then
|
|
35
35
|
automation_require_command opencode
|
|
36
36
|
fi
|
|
37
|
-
[[ -x "$AUTOMATION_ROOT/gradlew" ]] || automation_die "gradlew is missing or not executable"
|
|
38
|
-
|
|
39
37
|
automation_validate_config
|
|
38
|
+
[[ -x "$(automation_gradle_build_root)/gradlew" ]] || automation_die "selected Gradle build wrapper is missing or not executable"
|
|
40
39
|
workspace_strategy="$(automation_config_value '.workspaceStrategy')"
|
|
41
40
|
if [[ "$workspace_strategy" == "isolatedWorktree" ]]; then
|
|
42
41
|
automation_worktree_base >/dev/null
|
|
@@ -33,10 +33,19 @@ max_attempts=$((max_fix_loops + 1))
|
|
|
33
33
|
attempt_log="$evidence_dir/gate-cycle-$coding_cycle-attempt-$attempt.log"
|
|
34
34
|
started_at="$(automation_now)"
|
|
35
35
|
set +e
|
|
36
|
-
"$
|
|
36
|
+
if [[ "$(jq -r '.schemaVersion' "$contract")" -ge "7" ]]; then
|
|
37
|
+
node "$AUTOMATION_ROOT/automation/verification/recovery.cjs" --stage green "$contract" "$AUTOMATION_CONFIG" "$AUTOMATION_ROOT" "$evidence_dir" 2>&1 | tee "$attempt_log"
|
|
38
|
+
else
|
|
39
|
+
"$SCRIPT_DIR/verify-task.sh" "$task_id" 2>&1 | tee "$attempt_log"
|
|
40
|
+
fi
|
|
37
41
|
gate_status=${PIPESTATUS[0]}
|
|
38
42
|
set -e
|
|
39
43
|
|
|
44
|
+
if [[ "$(jq -r '.schemaVersion' "$contract")" -ge "7" && ( "$gate_status" -eq 75 || "$gate_status" -eq 76 ) ]]; then
|
|
45
|
+
automation_transition_state "$task_id" "CODING" "BLOCKED" "quality-gate" "verification stopped; inspect stage-recovery/green.json; implementation budget retained"
|
|
46
|
+
exit "$gate_status"
|
|
47
|
+
fi
|
|
48
|
+
|
|
40
49
|
jq -n \
|
|
41
50
|
--arg taskId "$task_id" \
|
|
42
51
|
--argjson attempts "$attempt" \
|
|
@@ -6,51 +6,201 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
6
6
|
source "$SCRIPT_DIR/lib.sh"
|
|
7
7
|
|
|
8
8
|
task_id="${1:-}"
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
if [[ "$schema_version" -ge "4" ]]; then
|
|
23
|
+
[[ "$#" -eq 1 ]] || automation_die "Schema V4/V5 usage: record-red.sh TASK-ID"
|
|
24
|
+
"$SCRIPT_DIR/scope-gate.sh" "$task_id" >/dev/null
|
|
25
|
+
if [[ "$schema_version" -ge "7" ]]; then
|
|
26
|
+
node "$AUTOMATION_ROOT/automation/verification/recovery.cjs" --stage red "$contract" "$AUTOMATION_CONFIG" "$AUTOMATION_ROOT" "$evidence_dir"
|
|
27
|
+
exit $?
|
|
28
|
+
fi
|
|
29
|
+
automation_run_inventory red "$task_id"
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Preserve the exact V1/V2 behavior for already approved contracts.
|
|
34
|
+
if [[ "$schema_version" != "3" ]]; then
|
|
35
|
+
expected="${2:-}"
|
|
36
|
+
separator="${3:-}"
|
|
37
|
+
filter="${4:-}"
|
|
38
|
+
if [[ -z "$expected" || "$separator" != "--" || -z "$filter" || "$#" -ne 4 ]]; then
|
|
39
|
+
printf 'Usage: %s TASK-ID EXPECTED-FAILURE-TEXT -- TEST-FILTER\n' "$0" >&2
|
|
40
|
+
exit 2
|
|
41
|
+
fi
|
|
42
|
+
automation_validate_test_filter "$filter"
|
|
43
|
+
[[ ${#expected} -ge 3 ]] || automation_die "expected failure text is too short"
|
|
44
|
+
target_count="$(jq -er --arg filter "$filter" '[.targetTests[] | select(.filter == $filter)] | length' "$contract")"
|
|
45
|
+
[[ "$target_count" -eq 1 ]] || automation_die "test filter must identify exactly one declared contract target: $filter"
|
|
46
|
+
gradle_task="$(jq -er --arg filter "$filter" '.targetTests[] | select(.filter == $filter) | .gradleTask' "$contract")"
|
|
47
|
+
red_log="$evidence_dir/red.log"
|
|
48
|
+
started_at="$(automation_now)"
|
|
49
|
+
set +e
|
|
50
|
+
automation_run_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT" 2>&1 | tee "$red_log"
|
|
51
|
+
red_status=${PIPESTATUS[0]}
|
|
52
|
+
set -e
|
|
53
|
+
[[ "$red_status" -ne 0 ]] || automation_die "RED capture failed: focused test passed before implementation"
|
|
54
|
+
rg -F "$expected" "$red_log" >/dev/null || automation_die "RED output does not contain the expected failure text"
|
|
55
|
+
jq -n --arg taskId "$task_id" --arg startedAt "$started_at" --arg finishedAt "$(automation_now)" \
|
|
56
|
+
--arg expectedFailure "$expected" --arg gradleTask "$gradle_task" --arg testFilter "$filter" \
|
|
57
|
+
--arg cwd "$(automation_gradle_build_root)" \
|
|
58
|
+
--argjson exitCode "$red_status" \
|
|
59
|
+
'{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt,
|
|
60
|
+
command: ["./gradlew", $gradleTask, "--tests", $testFilter], cwd: $cwd,
|
|
61
|
+
expectedFailure: $expectedFailure, exitCode: $exitCode}' | automation_record_json "$red_meta"
|
|
62
|
+
automation_info "$task_id legacy RED evidence recorded"
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
41
65
|
|
|
42
|
-
[[ "
|
|
43
|
-
|
|
66
|
+
[[ "$#" -eq 1 ]] || { printf 'Schema V3 usage: %s TASK-ID\n' "$0" >&2; exit 2; }
|
|
67
|
+
"$SCRIPT_DIR/scope-gate.sh" "$task_id" >/dev/null
|
|
68
|
+
|
|
69
|
+
# RED must be captured before any production change. Planning artifacts are
|
|
70
|
+
# excluded; test sources and fixtures are the only permitted product changes.
|
|
71
|
+
while IFS= read -r changed_path; do
|
|
72
|
+
[[ -n "$changed_path" ]] || continue
|
|
73
|
+
if ! automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$changed_path"; then
|
|
74
|
+
automation_die "RED preflight requires unchanged production code; non-test path changed: $changed_path"
|
|
75
|
+
fi
|
|
76
|
+
done < <(automation_product_changed_paths_at "$task_id" "$AUTOMATION_ROOT")
|
|
77
|
+
|
|
78
|
+
previous_attempt=0
|
|
79
|
+
preflight_meta="$evidence_dir/test-preflight.json"
|
|
80
|
+
if [[ -f "$preflight_meta" ]]; then
|
|
81
|
+
previous_attempt="$(jq -er '.attempt' "$preflight_meta")"
|
|
82
|
+
fi
|
|
83
|
+
attempt=$((previous_attempt + 1))
|
|
84
|
+
max_fixes="$(jq -er '.verification.maxPreparationFixes' "$contract")"
|
|
85
|
+
if [[ "$attempt" -gt $((max_fixes + 1)) ]]; then
|
|
86
|
+
automation_die "test preparation retry budget exhausted; revise the contract or abort the task"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
attempt_id="$(printf '%03d' "$attempt")"
|
|
90
|
+
attempt_dir="$evidence_dir/attempts/red-preflight-$attempt_id"
|
|
91
|
+
mkdir -p "$attempt_dir"
|
|
92
|
+
observed_file="$attempt_dir/observed.jsonl"
|
|
93
|
+
: > "$observed_file"
|
|
94
|
+
execution_failures='[]'
|
|
95
|
+
started_at="$(automation_now)"
|
|
96
|
+
target_index=0
|
|
97
|
+
while IFS=$'\t' read -r gradle_task filter; do
|
|
98
|
+
result_file="$attempt_dir/target-$target_index.jsonl"
|
|
99
|
+
log_file="$attempt_dir/target-$target_index.log"
|
|
100
|
+
set +e
|
|
101
|
+
automation_run_classified_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT" "$result_file" "$log_file"
|
|
102
|
+
gradle_status=$?
|
|
103
|
+
set -e
|
|
104
|
+
if [[ "$gradle_status" -ne 0 ]]; then
|
|
105
|
+
execution_failures="$(jq -nc --argjson current "$execution_failures" --argjson target "$target_index" \
|
|
106
|
+
--arg task "$gradle_task" --arg filter "$filter" --argjson exitCode "$gradle_status" \
|
|
107
|
+
'$current + [{target: $target, gradleTask: $task, filter: $filter, exitCode: $exitCode}]')"
|
|
108
|
+
fi
|
|
109
|
+
if [[ -s "$result_file" ]]; then
|
|
110
|
+
while IFS= read -r result_line; do
|
|
111
|
+
if ! jq -e . >/dev/null 2>&1 <<< "$result_line"; then
|
|
112
|
+
execution_failures="$(jq -nc --argjson current "$execution_failures" --argjson target "$target_index" \
|
|
113
|
+
'$current + [{target: $target, reason: "invalid structured test output"}]')"
|
|
114
|
+
continue
|
|
115
|
+
fi
|
|
116
|
+
jq -c --argjson target "$target_index" '. + {target: $target}' <<< "$result_line" >> "$observed_file"
|
|
117
|
+
done < "$result_file"
|
|
118
|
+
else
|
|
119
|
+
execution_failures="$(jq -nc --argjson current "$execution_failures" --argjson target "$target_index" \
|
|
120
|
+
'$current + [{target: $target, reason: "no structured test results"}]')"
|
|
121
|
+
fi
|
|
122
|
+
target_index=$((target_index + 1))
|
|
123
|
+
done < <(jq -r '.targetTests[] | [.gradleTask, .filter] | @tsv' "$contract")
|
|
124
|
+
|
|
125
|
+
contract_sha="$(automation_file_sha256 "$contract")"
|
|
126
|
+
baseline_head="$(jq -er '.head' "$evidence_dir/baseline.json")"
|
|
127
|
+
test_diff_sha="$(automation_test_diff_sha "$task_id" "$AUTOMATION_ROOT")"
|
|
128
|
+
finished_at="$(automation_now)"
|
|
44
129
|
|
|
45
130
|
jq -n \
|
|
131
|
+
--slurpfile contract "$contract" \
|
|
132
|
+
--slurpfile observed "$observed_file" \
|
|
46
133
|
--arg taskId "$task_id" \
|
|
47
134
|
--arg startedAt "$started_at" \
|
|
48
|
-
--arg finishedAt "$
|
|
49
|
-
--arg
|
|
50
|
-
--arg
|
|
51
|
-
--arg
|
|
52
|
-
--argjson
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
135
|
+
--arg finishedAt "$finished_at" \
|
|
136
|
+
--arg contractSha256 "$contract_sha" \
|
|
137
|
+
--arg baselineHead "$baseline_head" \
|
|
138
|
+
--arg testDiffSha256 "$test_diff_sha" \
|
|
139
|
+
--argjson attempt "$attempt" \
|
|
140
|
+
--argjson executionFailures "$execution_failures" '
|
|
141
|
+
def failure_matches($decl; $actual):
|
|
142
|
+
($actual.result == "FAILURE") and
|
|
143
|
+
($actual.exceptionType == $decl.expectedFailure.type) and
|
|
144
|
+
(($decl.expectedFailure.messageIncludes // "") as $fragment |
|
|
145
|
+
($fragment == "" or (($actual.exceptionMessage // "") | contains($fragment))));
|
|
146
|
+
($contract[0].verification.cases | map(
|
|
147
|
+
. as $decl |
|
|
148
|
+
[$observed[] | select(.kind == "case" and .target == $decl.test.target and
|
|
149
|
+
.className == $decl.test.className and .name == $decl.test.name)] as $matches |
|
|
150
|
+
(if ($matches | length) == 1 then $matches[0] else null end) as $actual |
|
|
151
|
+
(if ($matches | length) != 1 then false
|
|
152
|
+
elif $decl.before == "pass" then $actual.result == "SUCCESS"
|
|
153
|
+
elif $decl.before == "fail" then failure_matches($decl; $actual)
|
|
154
|
+
elif $actual.result == "SUCCESS" then true
|
|
155
|
+
elif ($decl | has("expectedFailure")) then failure_matches($decl; $actual)
|
|
156
|
+
else false end) as $valid |
|
|
157
|
+
{id: $decl.id, criterion: $decl.criterion, intent: $decl.intent,
|
|
158
|
+
expectedBefore: $decl.before, test: $decl.test, expectedFailure: ($decl.expectedFailure // null),
|
|
159
|
+
matches: ($matches | length), actual: $actual, valid: $valid}
|
|
160
|
+
)) as $cases |
|
|
161
|
+
[$observed[] | select(.kind == "case") as $actual |
|
|
162
|
+
select(any($contract[0].verification.cases[];
|
|
163
|
+
.test.target == $actual.target and .test.className == $actual.className and .test.name == $actual.name) | not)] as $undeclared |
|
|
164
|
+
[$observed[] | select(.kind == "suite")] as $suites |
|
|
165
|
+
($executionFailures | length == 0 and
|
|
166
|
+
($suites | length) >= ($contract[0].targetTests | length) and
|
|
167
|
+
($cases | all(.valid)) and ($undeclared | length == 0) and
|
|
168
|
+
($cases | any(.intent == "change" and .actual.result == "FAILURE"))) as $valid |
|
|
169
|
+
{taskId: $taskId, attempt: $attempt, startedAt: $startedAt, finishedAt: $finishedAt,
|
|
170
|
+
contractSha256: $contractSha256, baselineHead: $baselineHead,
|
|
171
|
+
testDiffSha256: $testDiffSha256, valid: $valid,
|
|
172
|
+
reasonCode: (if $valid then "VALID_RED"
|
|
173
|
+
elif ($executionFailures | length) > 0 then "EXECUTION_FAILURE"
|
|
174
|
+
elif ($undeclared | length) > 0 then "UNDECLARED_TEST_RESULT"
|
|
175
|
+
else "CASE_EXPECTATION_MISMATCH" end),
|
|
176
|
+
summary: {declared: ($cases | length), valid: ([$cases[] | select(.valid)] | length),
|
|
177
|
+
invalid: ([$cases[] | select(.valid | not)] | length),
|
|
178
|
+
expectedRed: ([$cases[] | select(.intent == "change" and .actual.result == "FAILURE")] | length),
|
|
179
|
+
undeclared: ($undeclared | length)},
|
|
180
|
+
cases: $cases, undeclaredCases: $undeclared, suites: $suites,
|
|
181
|
+
executionFailures: $executionFailures}' | automation_record_json "$preflight_meta"
|
|
182
|
+
|
|
183
|
+
cp "$preflight_meta" "$attempt_dir/evaluation.json"
|
|
184
|
+
if [[ "$(jq -r '.valid' "$preflight_meta")" != "true" ]]; then
|
|
185
|
+
reason="$(jq -r '.reasonCode' "$preflight_meta")"
|
|
186
|
+
invalid="$(jq -r '[.cases[] | select(.valid | not) | .id] | join(", ")' "$preflight_meta")"
|
|
187
|
+
automation_die "RED preflight rejected ($reason); invalid cases: ${invalid:-none}; evidence: $preflight_meta"
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
jq '{taskId, contractSha256, baselineHead, testDiffSha256,
|
|
191
|
+
cases: [.cases[] | {id, criterion, intent, test, actual}]}' "$preflight_meta" \
|
|
192
|
+
| automation_record_json "$evidence_dir/test-manifest.json"
|
|
193
|
+
preflight_sha="$(automation_file_sha256 "$preflight_meta")"
|
|
194
|
+
manifest_sha="$(automation_file_sha256 "$evidence_dir/test-manifest.json")"
|
|
195
|
+
jq -n --arg taskId "$task_id" --arg startedAt "$started_at" --arg finishedAt "$finished_at" \
|
|
196
|
+
--arg contractSha256 "$contract_sha" --arg baselineHead "$baseline_head" \
|
|
197
|
+
--arg testDiffSha256 "$test_diff_sha" --arg preflightSha256 "$preflight_sha" \
|
|
198
|
+
--arg manifestSha256 "$manifest_sha" --arg attemptPath "attempts/red-preflight-$attempt_id" \
|
|
199
|
+
--argjson attempt "$attempt" \
|
|
200
|
+
'{taskId: $taskId, schemaVersion: 3, startedAt: $startedAt, finishedAt: $finishedAt,
|
|
201
|
+
contractSha256: $contractSha256, baselineHead: $baselineHead,
|
|
202
|
+
testDiffSha256: $testDiffSha256, preflightSha256: $preflightSha256,
|
|
203
|
+
manifestSha256: $manifestSha256, attempt: $attempt, attemptPath: $attemptPath,
|
|
204
|
+
structuredCasesVerified: true, exitCode: 1}' | automation_record_json "$red_meta"
|
|
205
|
+
|
|
206
|
+
automation_info "$task_id structured RED evidence recorded"
|
|
@@ -55,6 +55,9 @@ source_root="$(cd "$source_root" && pwd -P)"
|
|
|
55
55
|
git -C "$source_root" show-ref --verify --quiet "refs/heads/$original_branch" || automation_die "recorded original branch no longer exists"
|
|
56
56
|
[[ "$(git -C "$source_root" rev-parse "refs/heads/$original_branch")" == "$baseline_head" ]] || automation_die "original branch drifted after task preparation"
|
|
57
57
|
|
|
58
|
+
if [[ "$(jq -r '.schemaVersion' "$(automation_contract_path "$task_id")")" == "4" && -e "$evidence_dir/baseline.json" ]]; then
|
|
59
|
+
automation_die "V4 baseline.json already exists; /resume-task cannot retry inventory capture. Inspect the retained attempt, request approved abort/archive, then approve a new task; do not delete or overwrite baseline evidence."
|
|
60
|
+
fi
|
|
58
61
|
for sealed_evidence in baseline.json red.json ready.json review.json reviews.jsonl; do
|
|
59
62
|
[[ ! -e "$evidence_dir/$sealed_evidence" ]] || \
|
|
60
63
|
automation_die "$sealed_evidence already exists; baseline-only recovery is forbidden"
|
|
@@ -66,7 +66,7 @@ while IFS= read -r path; do
|
|
|
66
66
|
if automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.productionPaths' "$path"; then
|
|
67
67
|
production_changed=1
|
|
68
68
|
fi
|
|
69
|
-
if
|
|
69
|
+
if automation_is_task_test_path "$task_id" "$path"; then
|
|
70
70
|
test_changed=1
|
|
71
71
|
fi
|
|
72
72
|
done <<< "$changed_file_list"
|
|
@@ -75,7 +75,7 @@ deleted_tests="$(
|
|
|
75
75
|
while IFS= read -r path; do
|
|
76
76
|
[[ -n "$path" ]] || continue
|
|
77
77
|
if [[ -n "$(git -C "$AUTOMATION_ROOT" diff --no-renames --name-only --diff-filter=D HEAD -- "$path")" ]] && \
|
|
78
|
-
|
|
78
|
+
automation_is_task_test_path "$task_id" "$path"; then
|
|
79
79
|
printf '%s\n' "$path"
|
|
80
80
|
fi
|
|
81
81
|
done <<< "$changed_file_list"
|
|
@@ -91,7 +91,7 @@ fi
|
|
|
91
91
|
added_test_lines="$(
|
|
92
92
|
while IFS= read -r path; do
|
|
93
93
|
[[ -n "$path" ]] || continue
|
|
94
|
-
if
|
|
94
|
+
if automation_is_task_test_path "$task_id" "$path"; then
|
|
95
95
|
git -C "$AUTOMATION_ROOT" diff --no-renames --unified=0 HEAD -- "$path"
|
|
96
96
|
fi
|
|
97
97
|
done <<< "$changed_file_list" \
|
|
@@ -106,7 +106,7 @@ fi
|
|
|
106
106
|
while IFS= read -r untracked_test; do
|
|
107
107
|
[[ -n "$untracked_test" ]] || continue
|
|
108
108
|
if ! git -C "$AUTOMATION_ROOT" ls-files --error-unmatch -- "$untracked_test" >/dev/null 2>&1 && \
|
|
109
|
-
|
|
109
|
+
automation_is_task_test_path "$task_id" "$untracked_test" && \
|
|
110
110
|
rg -n "$weakening_pattern" "$AUTOMATION_ROOT/$untracked_test" >/dev/null; then
|
|
111
111
|
automation_die "potential test weakening detected in new test: $untracked_test"
|
|
112
112
|
exit 40
|
|
@@ -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")" \
|
|
@@ -88,6 +95,7 @@ printf -- '- Reviewer:`%s`,独立复验退出码 `%s`\n' \
|
|
|
88
95
|
"$(jq -r '.evidence.reviewerVerificationExitCode' "$report_file")"
|
|
89
96
|
printf -- '- Reviewer 摘要:%s\n' "$(jq -r '.reviewSummary' "$report_file")"
|
|
90
97
|
printf -- '- 聚焦测试:\n'
|
|
98
|
+
printf -- ' - Gradle 工作目录:`%s`\n' "$(automation_gradle_build_root)"
|
|
91
99
|
jq -r '.targetTests[] | " - `./gradlew \(.gradleTask) --tests \(.filter)`"' "$report_file"
|
|
92
100
|
|
|
93
101
|
printf '\n### P1 · 绑定与剩余风险\n\n'
|