@frankzhang2026/opencode-android-orchestrator 0.2.0 → 0.3.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +50 -39
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/dist/cli.js +127 -10
  5. package/dist/cli.js.map +1 -1
  6. package/dist/doctor/installation.d.ts.map +1 -1
  7. package/dist/doctor/installation.js +29 -4
  8. package/dist/doctor/installation.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/installer/adaptive-templates.d.ts +25 -3
  14. package/dist/installer/adaptive-templates.d.ts.map +1 -1
  15. package/dist/installer/adaptive-templates.js +75 -9
  16. package/dist/installer/adaptive-templates.js.map +1 -1
  17. package/dist/installer/index.d.ts +2 -2
  18. package/dist/installer/index.d.ts.map +1 -1
  19. package/dist/installer/index.js +2 -2
  20. package/dist/installer/index.js.map +1 -1
  21. package/dist/installer/init.d.ts +2 -1
  22. package/dist/installer/init.d.ts.map +1 -1
  23. package/dist/installer/init.js +10 -1
  24. package/dist/installer/init.js.map +1 -1
  25. package/dist/installer/opencode-config.d.ts +4 -3
  26. package/dist/installer/opencode-config.d.ts.map +1 -1
  27. package/dist/installer/opencode-config.js +10 -4
  28. package/dist/installer/opencode-config.js.map +1 -1
  29. package/dist/installer/upgrade.d.ts +5 -0
  30. package/dist/installer/upgrade.d.ts.map +1 -1
  31. package/dist/installer/upgrade.js +44 -10
  32. package/dist/installer/upgrade.js.map +1 -1
  33. package/docs/MIGRATION.md +23 -12
  34. package/docs/SECURITY.md +11 -4
  35. package/docs/TROUBLESHOOTING.md +5 -6
  36. package/package.json +1 -1
  37. package/templates/README.md +5 -2
  38. package/templates/automation/config.json +17 -0
  39. package/templates/automation/config.schema.json +31 -0
  40. package/templates/automation/task-contract.schema.json +22 -1
  41. package/templates/automation/tasks/TASK-TEMPLATE.json.example +4 -1
  42. package/templates/scripts/automation/claim-task.sh +6 -6
  43. package/templates/scripts/automation/lib.sh +118 -0
  44. package/templates/scripts/automation/preflight.sh +1 -1
  45. package/templates/scripts/automation/record-red.sh +7 -9
  46. package/templates/scripts/automation/show-acceptance-review.sh +1 -1
  47. package/templates/scripts/automation/tests/run-tests.sh +26 -9
  48. package/templates/scripts/automation/validate-contract.sh +16 -5
  49. package/templates/scripts/automation/verify-integration.sh +12 -11
  50. package/templates/scripts/automation/verify-task.sh +13 -28
package/docs/MIGRATION.md CHANGED
@@ -1,16 +1,17 @@
1
1
  # Migration guide
2
2
 
3
3
  This guide covers migration to
4
- `@frankzhang2026/opencode-android-orchestrator@0.2.0` after that exact version
5
- has been released and accepted. Version `0.2.0` is currently unpublished; do
6
- not use these commands to switch a live project yet.
4
+ `@frankzhang2026/opencode-android-orchestrator@0.3.0`. Pin the exact version and
5
+ prove the migration in a disposable clone before changing a long-lived
6
+ repository.
7
7
 
8
8
  ## Choose the migration path
9
9
 
10
10
  | Current state | Correct command after release | Important distinction |
11
11
  | --- | --- | --- |
12
- | No orchestrator files or manifest | `npx @frankzhang2026/opencode-android-orchestrator@0.2.0 init .` | Normal new installation. |
12
+ | No orchestrator files or manifest | `npx @frankzhang2026/opencode-android-orchestrator@0.3.0 init .` | Normal new installation; all detected Android modules are enabled by default. |
13
13
  | Published `0.1.0` scaffold only | Remove any project-local `@0.1.0` plugin reference after review, then run `init`. | `0.1.0` did not create a usable managed installation and cannot be upgraded. |
14
+ | Healthy `0.2.0` manifest-managed installation | Run the fixed `0.3.0` doctor, then `upgrade`. | The missing module-scope field is treated as `primary` unless the upgrade explicitly selects `all`. |
14
15
  | Manually copied V3 files, no `.automation-plugin/manifest.json` | Finish active tasks, preserve historical evidence separately, then run `init`. | Exact files can be reused; differing managed files fail as conflicts. |
15
16
  | Healthy older manifest-managed installation | Run `doctor`, then the fixed target version's `upgrade`. | `upgrade` requires a valid installed manifest and intact original backups. |
16
17
  | Healthy current-version manifest | Run `doctor`; repeated `init` or same-version `upgrade` is verification-only and byte-idempotent. | Do not reinstall or delete the manifest. |
@@ -35,7 +36,7 @@ removes unchanged plugin-created files, and retains drift for manual review.
35
36
  5. If a managed manifest already exists, run:
36
37
 
37
38
  ```sh
38
- npx @frankzhang2026/opencode-android-orchestrator@0.2.0 doctor . --json
39
+ npx @frankzhang2026/opencode-android-orchestrator@0.3.0 doctor . --json
39
40
  ```
40
41
 
41
42
  Do not proceed with `upgrade` unless the installation checks pass.
@@ -51,18 +52,22 @@ scaffold, not as an older managed installation.
51
52
  If the project OpenCode configuration contains an exact
52
53
  `@frankzhang2026/opencode-android-orchestrator@0.1.0` entry, save the file and
53
54
  remove only that obsolete entry in a reviewed Git change before running
54
- `0.2.0 init`. The merger deliberately rejects a different version of the same
55
+ `0.3.0 init`. The merger deliberately rejects a different version of the same
55
56
  managed package; it will not silently replace the reference. A global npm
56
57
  installation of `0.1.0` alone does not require project-file cleanup.
57
58
 
58
59
  After release, initialize with the fixed version:
59
60
 
60
61
  ```sh
61
- npx @frankzhang2026/opencode-android-orchestrator@0.2.0 init .
62
+ npx @frankzhang2026/opencode-android-orchestrator@0.3.0 init .
62
63
  ```
63
64
 
64
- Use `--primary-module :mobile` when the project has multiple application
65
- modules and `:mobile` is the intended orchestration target.
65
+ New installations default to all-module scope, so multiple application modules
66
+ do not require a selection. Use
67
+ `--module-scope primary --primary-module :mobile` only when generated task
68
+ contracts must be restricted to `:mobile`. In all-module scope,
69
+ `--primary-module :mobile` merely chooses the focused-test placeholder's
70
+ default module.
66
71
 
67
72
  ## From a manually copied V3 setup
68
73
 
@@ -100,8 +105,8 @@ installer to guess which customization should survive.
100
105
  Use the lifecycle command selected by the active manifest:
101
106
 
102
107
  ```sh
103
- npx @frankzhang2026/opencode-android-orchestrator@0.2.0 doctor . --json
104
- npx @frankzhang2026/opencode-android-orchestrator@0.2.0 upgrade . --json
108
+ npx @frankzhang2026/opencode-android-orchestrator@0.3.0 doctor . --json
109
+ npx @frankzhang2026/opencode-android-orchestrator@0.3.0 upgrade . --json
105
110
  ```
106
111
 
107
112
  `upgrade` verifies the installed manifest, every managed file, and every
@@ -110,6 +115,12 @@ OpenCode and AGENTS content from the original pre-install files, carries that
110
115
  recovery lineage forward, snapshots the current version, writes the new
111
116
  resources, and reruns the 38 automation tests plus the shadow run.
112
117
 
118
+ Upgrade preserves an installed `androidProject.moduleScope`. A legacy
119
+ manifest-managed configuration without that field is treated as `primary`,
120
+ which prevents an upgrade from silently expanding its editable module set. To
121
+ adopt all-module scope during a version upgrade, pass `--module-scope all` and
122
+ review the regenerated task example before approving automation.
123
+
113
124
  The command refuses:
114
125
 
115
126
  - a downgrade or malformed semantic version;
@@ -127,7 +138,7 @@ the source of drift and use the recorded recovery data.
127
138
  Run all checks from the detected Git root:
128
139
 
129
140
  ```sh
130
- npx @frankzhang2026/opencode-android-orchestrator@0.2.0 doctor .
141
+ npx @frankzhang2026/opencode-android-orchestrator@0.3.0 doctor .
131
142
  opencode debug config
132
143
  opencode debug agent scheduled-planner
133
144
  opencode debug agent scheduled-coder
package/docs/SECURITY.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Security model
2
2
 
3
3
  This document describes the security properties of
4
- `@frankzhang2026/opencode-android-orchestrator@0.2.0`. Version `0.2.0` is
5
- currently unpublished and has not completed the real OpenCode `1.14.22` and
6
- `1.15.13` release matrix.
4
+ `@frankzhang2026/opencode-android-orchestrator@0.3.0`. The lifecycle foundation
5
+ completed the real OpenCode `1.14.22` and `1.15.13` release matrix in `0.2.0`;
6
+ `0.3.0` retains that compatibility boundary.
7
7
 
8
8
  ## Security goals and non-goals
9
9
 
@@ -52,7 +52,7 @@ modify local files; this package does not claim to protect against that actor.
52
52
 
53
53
  OpenCode permission prompts are also not semantic workflow approval. They can
54
54
  be accepted for the remainder of a session and may be auto-approved. For that
55
- reason `0.2.0` exposes only `android_orchestrator_status` and
55
+ reason `0.3.0` exposes only `android_orchestrator_status` and
56
56
  `android_orchestrator_doctor` as custom tools. State-changing wrappers remain a
57
57
  NO-GO until a one-use, non-model-forgeable receipt can bind the approval kind,
58
58
  task, session/message, sealed SHA or branch, time, and nonce.
@@ -105,6 +105,13 @@ plugin options. It rejects malformed/ambiguous files, duplicate identities,
105
105
  different managed-plugin versions, and symlinks. AGENTS merging owns only one
106
106
  marked block and rejects partial, duplicate, or modified markers.
107
107
 
108
+ New installations generate task examples in `all` module scope: every detected
109
+ Android module's `src/main`, `src/test`, and `src/androidTest` path is eligible,
110
+ while Gradle settings/build files and orchestration resources remain protected.
111
+ `primary` scope narrows the generated paths to one module. Upgrade treats a
112
+ legacy configuration with no scope field as `primary`, preventing an implicit
113
+ permission expansion.
114
+
108
115
  The installed manifest is `0600`. Installer control, backup, recovery, and
109
116
  history directories are created with private `0700` defaults; backup files
110
117
  preserve the original file mode where recovery requires it. Shell resources
@@ -1,9 +1,7 @@
1
1
  # Troubleshooting
2
2
 
3
3
  Use this guide for
4
- `@frankzhang2026/opencode-android-orchestrator@0.2.0`. That version is currently
5
- unpublished, so lifecycle commands shown here are post-release commands unless
6
- you are working in an isolated maintainer fixture.
4
+ `@frankzhang2026/opencode-android-orchestrator@0.3.0`.
7
5
 
8
6
  ## Start with read-only evidence
9
7
 
@@ -13,7 +11,7 @@ From the repository root, capture:
13
11
  git status --short --branch
14
12
  git rev-parse HEAD
15
13
  opencode --version
16
- npx @frankzhang2026/opencode-android-orchestrator@0.2.0 doctor . --json
14
+ npx @frankzhang2026/opencode-android-orchestrator@0.3.0 doctor . --json
17
15
  ```
18
16
 
19
17
  If installation never completed, doctor will correctly report a missing or
@@ -41,8 +39,9 @@ remain human-readable on stderr with a stable code such as `[FILE_CONFLICT]`.
41
39
  | OpenCode version failure | Installed version is below `1.14.22`, at or above `1.16.0`, or cannot be parsed/executed. | Install a certified version (`1.14.22` or `1.15.13`) for release validation. Do not bypass the version gate. |
42
40
  | Git root or Android project not detected | The target is outside a Git repository, settings are missing, or no supported Android module was found. | Run from the intended repository/module and inspect `settings.gradle` or `settings.gradle.kts`. Nested Gradle roots are intentionally unsupported. |
43
41
  | Gradle Wrapper failure | `gradlew` or `gradle/wrapper/gradle-wrapper.properties` is missing, or `gradlew` is not executable. | Restore the project's reviewed Wrapper files. Do not let the installer generate or replace build configuration. |
44
- | `PRIMARY_MODULE_AMBIGUOUS` | Multiple Android application modules exist. | Rerun `init` or `upgrade` with an exact Gradle path such as `--primary-module :mobile`. |
45
- | `PRIMARY_MODULE_NOT_FOUND` | The selected Gradle path is absent or not an application module. | Use a module path reported by doctor/project detection; do not pass a filesystem directory. |
42
+ | `MODULE_SCOPE_INVALID` or an invalid `--module-scope` argument | The value is not `all` or `primary`. | Use `all` for the default all-module contract or `primary` for an intentional single-module restriction. |
43
+ | `PRIMARY_MODULE_AMBIGUOUS` | Restrictive `primary` scope has multiple possible Android modules. | Supply an exact Gradle path, for example `--module-scope primary --primary-module :mobile`, or use the default `all` scope. |
44
+ | `PRIMARY_MODULE_NOT_FOUND` | The selected Gradle path was not detected. | Use a module path reported by doctor/project detection; do not pass a filesystem directory. |
46
45
  | Android SDK failure | No valid explicit SDK, `ANDROID_HOME`, `ANDROID_SDK_ROOT`, or `local.properties` `sdk.dir` was found. | Configure one real SDK root containing `platforms/` and `build-tools/`. Do not publish `local.properties`. |
47
46
  | Missing `git`, `jq`, `rg`, `shasum`, or Java | Required deterministic command is unavailable on `PATH`. | Install or restore the missing command, record its version, and rerun the read-only checks. |
48
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frankzhang2026/opencode-android-orchestrator",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Reusable OpenCode orchestration for Android projects",
5
5
  "license": "MIT",
6
6
  "author": "frankzhang2026",
@@ -25,8 +25,11 @@ Migrated template roots:
25
25
  `automation/config.json` is an input to the adaptive renderer, not a directly
26
26
  installable configuration. `planAdaptiveProjectTemplates` injects the required
27
27
  `androidProject` metadata, repository-relative build files, protected paths,
28
- all detected production/test source sets, and primary-module task paths. The
29
- renderer rejects ambiguous primary modules unless the caller selects one.
28
+ all detected production/test source sets, and a module scope. New renders use
29
+ `all` scope and generate task paths for every detected module without requiring
30
+ a primary selection. `primary` scope restricts paths to one module and retains
31
+ the ambiguity guard. The selected/default module still supplies the focused
32
+ test placeholder in both modes.
30
33
 
31
34
  The legacy Scheduler field has been removed while Superpowers remains pinned.
32
35
  The scope gates consume generated source-set arrays, and the Shell test fixture
@@ -29,6 +29,23 @@
29
29
  "systematic-debugging",
30
30
  "verification-before-completion"
31
31
  ],
32
+ "gradleVerification": {
33
+ "fullUnitTestTasks": [
34
+ "testDebugUnitTest"
35
+ ],
36
+ "focusedTestTasks": [
37
+ "testDebugUnitTest"
38
+ ],
39
+ "assembleTasks": [
40
+ "assembleDebug"
41
+ ],
42
+ "lintTasks": [
43
+ "lint"
44
+ ],
45
+ "deviceTestTasks": [
46
+ "connectedDebugAndroidTest"
47
+ ]
48
+ },
32
49
  "protectedPaths": [
33
50
  ".opencode/",
34
51
  ".automation-plugin/",
@@ -13,6 +13,17 @@
13
13
  "nullableIdentifier": {
14
14
  "type": ["string", "null"],
15
15
  "minLength": 1
16
+ },
17
+ "gradleTask": {
18
+ "type": "string",
19
+ "minLength": 1,
20
+ "pattern": "^(?:[A-Za-z][A-Za-z0-9_.-]*|(?::[A-Za-z0-9_.-]+)+)$"
21
+ },
22
+ "gradleTaskList": {
23
+ "type": "array",
24
+ "minItems": 1,
25
+ "uniqueItems": true,
26
+ "items": { "$ref": "#/$defs/gradleTask" }
16
27
  }
17
28
  },
18
29
  "required": [
@@ -30,6 +41,7 @@
30
41
  "approvalPhrases",
31
42
  "plugins",
32
43
  "requiredSkills",
44
+ "gradleVerification",
33
45
  "androidProject",
34
46
  "protectedPaths"
35
47
  ],
@@ -72,6 +84,24 @@
72
84
  "uniqueItems": true,
73
85
  "items": { "type": "string", "minLength": 1 }
74
86
  },
87
+ "gradleVerification": {
88
+ "type": "object",
89
+ "additionalProperties": false,
90
+ "required": [
91
+ "fullUnitTestTasks",
92
+ "focusedTestTasks",
93
+ "assembleTasks",
94
+ "lintTasks",
95
+ "deviceTestTasks"
96
+ ],
97
+ "properties": {
98
+ "fullUnitTestTasks": { "$ref": "#/$defs/gradleTaskList" },
99
+ "focusedTestTasks": { "$ref": "#/$defs/gradleTaskList" },
100
+ "assembleTasks": { "$ref": "#/$defs/gradleTaskList" },
101
+ "lintTasks": { "$ref": "#/$defs/gradleTaskList" },
102
+ "deviceTestTasks": { "$ref": "#/$defs/gradleTaskList" }
103
+ }
104
+ },
75
105
  "androidProject": {
76
106
  "type": "object",
77
107
  "additionalProperties": false,
@@ -88,6 +118,7 @@
88
118
  "name": { "type": "string", "minLength": 1 },
89
119
  "gradleDsl": { "enum": ["kotlin", "groovy", "mixed"] },
90
120
  "settingsFile": { "$ref": "#/$defs/repositoryPath" },
121
+ "moduleScope": { "enum": ["all", "primary"] },
91
122
  "primaryModule": {
92
123
  "type": "string",
93
124
  "pattern": "^:(?:[A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)*)?$"
@@ -4,6 +4,18 @@
4
4
  "title": "Scheduled coding task contract",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
+ "$defs": {
8
+ "gradleTask": {
9
+ "type": "string",
10
+ "minLength": 1,
11
+ "pattern": "^(?:[A-Za-z][A-Za-z0-9_.-]*|(?::[A-Za-z0-9_.-]+)+)$"
12
+ },
13
+ "testFilter": {
14
+ "type": "string",
15
+ "minLength": 1,
16
+ "pattern": "^[A-Za-z0-9_.#$*-]+$"
17
+ }
18
+ },
7
19
  "required": [
8
20
  "schemaVersion",
9
21
  "id",
@@ -62,7 +74,16 @@
62
74
  "targetTests": {
63
75
  "type": "array",
64
76
  "minItems": 1,
65
- "items": { "type": "string", "minLength": 1 }
77
+ "uniqueItems": true,
78
+ "items": {
79
+ "type": "object",
80
+ "additionalProperties": false,
81
+ "required": ["gradleTask", "filter"],
82
+ "properties": {
83
+ "gradleTask": { "$ref": "#/$defs/gradleTask" },
84
+ "filter": { "$ref": "#/$defs/testFilter" }
85
+ }
86
+ }
66
87
  },
67
88
  "deviceTestsRequired": { "type": "boolean" },
68
89
  "testPolicy": { "enum": ["required", "not-required"] },
@@ -45,7 +45,10 @@
45
45
  "Do not upgrade dependencies or Gradle configuration"
46
46
  ],
47
47
  "targetTests": [
48
- "*ReplaceWithFocusedTest"
48
+ {
49
+ "gradleTask": "testDebugUnitTest",
50
+ "filter": "*ReplaceWithFocusedTest"
51
+ }
49
52
  ],
50
53
  "deviceTestsRequired": false,
51
54
  "testPolicy": "required",
@@ -30,12 +30,10 @@ baseline_meta="$evidence_dir/baseline.json"
30
30
  protected_hashes="$evidence_dir/protected.sha256"
31
31
  started_at="$(automation_now)"
32
32
  head_commit="$(git -C "$AUTOMATION_ROOT" rev-parse HEAD)"
33
+ baseline_command="$(automation_gradle_group_command_json "fullUnitTestTasks")"
33
34
 
34
35
  set +e
35
- (
36
- cd "$AUTOMATION_ROOT"
37
- ./gradlew testDebugUnitTest
38
- ) 2>&1 | tee "$baseline_log"
36
+ automation_run_gradle_group "fullUnitTestTasks" "$AUTOMATION_ROOT" 2>&1 | tee "$baseline_log"
39
37
  baseline_status=${PIPESTATUS[0]}
40
38
  set -e
41
39
 
@@ -45,8 +43,9 @@ if [[ "$baseline_status" -ne 0 ]]; then
45
43
  --arg startedAt "$started_at" \
46
44
  --arg finishedAt "$(automation_now)" \
47
45
  --arg head "$head_commit" \
46
+ --argjson command "$baseline_command" \
48
47
  --argjson exitCode "$baseline_status" \
49
- '{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt, head: $head, command: "./gradlew testDebugUnitTest", exitCode: $exitCode}' \
48
+ '{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt, head: $head, command: $command, exitCode: $exitCode}' \
50
49
  | automation_record_json "$baseline_meta"
51
50
  automation_transition_state "$task_id" "CODING" "BLOCKED" "preflight" "baseline unit tests failed"
52
51
  automation_die "baseline unit tests failed"
@@ -68,7 +67,8 @@ jq -n \
68
67
  --arg finishedAt "$(automation_now)" \
69
68
  --arg head "$head_commit" \
70
69
  --arg worktree "$AUTOMATION_ROOT" \
71
- '{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt, head: $head, worktree: $worktree, command: "./gradlew testDebugUnitTest", exitCode: 0}' \
70
+ --argjson command "$baseline_command" \
71
+ '{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt, head: $head, worktree: $worktree, command: $command, exitCode: 0}' \
72
72
  | automation_record_json "$baseline_meta"
73
73
 
74
74
  automation_info "$task_id claimed; baseline is green"
@@ -69,6 +69,23 @@ automation_require_command() {
69
69
  command -v "$1" >/dev/null 2>&1 || automation_die "required command is missing: $1"
70
70
  }
71
71
 
72
+ automation_resolve_opencode_config() {
73
+ local output_file="$1"
74
+ local error_file="$2"
75
+ local checkpoint_error="Failed to run the query 'PRAGMA wal_checkpoint(PASSIVE)'"
76
+
77
+ if opencode debug config > "$output_file" 2> "$error_file"; then
78
+ return 0
79
+ fi
80
+ if ! rg -F "$checkpoint_error" "$error_file" >/dev/null; then
81
+ return 1
82
+ fi
83
+
84
+ automation_warn "OpenCode database checkpoint failed once; retrying resolved config discovery"
85
+ sleep 1
86
+ opencode debug config > "$output_file" 2> "$error_file"
87
+ }
88
+
72
89
  automation_require_layout() {
73
90
  [[ -f "$AUTOMATION_CONFIG" ]] || automation_die "missing $AUTOMATION_CONFIG"
74
91
  [[ -d "$AUTOMATION_TASKS_DIR" ]] || automation_die "missing $AUTOMATION_TASKS_DIR"
@@ -83,6 +100,14 @@ automation_validate_config() {
83
100
  (startswith("/") | not) and
84
101
  (test("^[A-Za-z]:[/\\\\]") | not) and
85
102
  (test("(^|/)\\.\\.(/|$)") | not);
103
+ def gradle_task:
104
+ type == "string" and
105
+ test("^(?:[A-Za-z][A-Za-z0-9_.-]*|(?::[A-Za-z0-9_.-]+)+)$");
106
+ def gradle_task_list:
107
+ type == "array" and
108
+ length > 0 and
109
+ length == (unique | length) and
110
+ all(.[]; gradle_task);
86
111
  .schemaVersion == 3 and
87
112
  (.enabled | type == "boolean") and
88
113
  (.mode == "shadow" or .mode == "orchestrated") and
@@ -101,11 +126,26 @@ automation_validate_config() {
101
126
  (.plugins | type == "object" and keys == ["superpowers"]) and
102
127
  (.plugins.superpowers | type == "string" and length > 0) and
103
128
  (.requiredSkills | type == "array" and length >= 6) and
129
+ (.gradleVerification as $verification |
130
+ ($verification | type == "object") and
131
+ ($verification | keys == [
132
+ "assembleTasks",
133
+ "deviceTestTasks",
134
+ "focusedTestTasks",
135
+ "fullUnitTestTasks",
136
+ "lintTasks"
137
+ ]) and
138
+ ($verification.fullUnitTestTasks | gradle_task_list) and
139
+ ($verification.focusedTestTasks | gradle_task_list) and
140
+ ($verification.assembleTasks | gradle_task_list) and
141
+ ($verification.lintTasks | gradle_task_list) and
142
+ ($verification.deviceTestTasks | gradle_task_list)) and
104
143
  (.androidProject as $project |
105
144
  ($project | type == "object") and
106
145
  ($project.name | type == "string" and length > 0) and
107
146
  ($project.gradleDsl == "kotlin" or $project.gradleDsl == "groovy" or $project.gradleDsl == "mixed") and
108
147
  ($project.settingsFile | repository_path) and
148
+ ((($project | has("moduleScope")) | not) or $project.moduleScope == "all" or $project.moduleScope == "primary") and
109
149
  ($project.primaryModule | type == "string" and test("^:(?:[A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)*)?$")) and
110
150
  ($project.modules | type == "array" and length > 0 and all(.[];
111
151
  (.gradlePath | type == "string" and test("^:(?:[A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)*)?$")) and
@@ -201,6 +241,84 @@ automation_config_value() {
201
241
  jq -r "$query" "$AUTOMATION_CONFIG"
202
242
  }
203
243
 
244
+ automation_validate_gradle_task() {
245
+ local task="${1:-}"
246
+ local safe_task_pattern='^([A-Za-z][A-Za-z0-9_.-]*|(:[A-Za-z0-9_.-]+)+)$'
247
+ [[ "$task" =~ $safe_task_pattern ]] || automation_die "unsafe Gradle task: ${task:-<empty>}"
248
+ }
249
+
250
+ automation_validate_test_filter() {
251
+ local filter="${1:-}"
252
+ local safe_filter_pattern='^[A-Za-z0-9_.#$*-]+$'
253
+ [[ "$filter" =~ $safe_filter_pattern ]] || automation_die "unsafe test filter: ${filter:-<empty>}"
254
+ }
255
+
256
+ automation_validate_gradle_group() {
257
+ local group="${1:-}"
258
+ case "$group" in
259
+ fullUnitTestTasks|focusedTestTasks|assembleTasks|lintTasks|deviceTestTasks) ;;
260
+ *)
261
+ automation_die "unknown Gradle verification group: ${group:-<empty>}"
262
+ return 1
263
+ ;;
264
+ esac
265
+ }
266
+
267
+ automation_gradle_group_command_json() {
268
+ local group="$1"
269
+ automation_validate_gradle_group "$group" || return 1
270
+ automation_validate_config || return 1
271
+ jq -ce \
272
+ --arg group "$group" \
273
+ '["./gradlew"] + .gradleVerification[$group]' \
274
+ "$AUTOMATION_CONFIG"
275
+ }
276
+
277
+ automation_run_gradle_group() {
278
+ local group="$1"
279
+ local root="${2:-$AUTOMATION_ROOT}"
280
+ local task
281
+ local -a tasks=()
282
+
283
+ automation_validate_gradle_group "$group" || return 1
284
+ automation_validate_config || return 1
285
+ while IFS= read -r task; do
286
+ automation_validate_gradle_task "$task" || return 1
287
+ tasks[${#tasks[@]}]="$task"
288
+ done < <(jq -er --arg group "$group" '.gradleVerification[$group][]' "$AUTOMATION_CONFIG")
289
+ [[ "${#tasks[@]}" -gt 0 ]] || {
290
+ automation_die "Gradle verification group is empty: $group"
291
+ return 1
292
+ }
293
+
294
+ (
295
+ cd "$root"
296
+ ./gradlew "${tasks[@]}"
297
+ )
298
+ }
299
+
300
+ automation_run_focused_test() {
301
+ local task="$1"
302
+ local filter="$2"
303
+ local root="${3:-$AUTOMATION_ROOT}"
304
+
305
+ automation_validate_config || return 1
306
+ automation_validate_gradle_task "$task" || return 1
307
+ automation_validate_test_filter "$filter" || return 1
308
+ jq -e \
309
+ --arg task "$task" \
310
+ '.gradleVerification.focusedTestTasks | index($task) != null' \
311
+ "$AUTOMATION_CONFIG" >/dev/null || {
312
+ automation_die "focused Gradle task is not allowed by automation/config.json: $task"
313
+ return 1
314
+ }
315
+
316
+ (
317
+ cd "$root"
318
+ ./gradlew "$task" --tests "$filter"
319
+ )
320
+ }
321
+
204
322
  automation_require_approval() {
205
323
  local kind="$1"
206
324
  local supplied="$2"
@@ -124,7 +124,7 @@ else
124
124
  discovery_dir="$(mktemp -d "${TMPDIR:-/tmp}/opencode-android-orchestrator-preflight.XXXXXX")"
125
125
  trap 'rm -rf "$discovery_dir"' EXIT
126
126
 
127
- if ! opencode debug config > "$discovery_dir/config.json" 2> "$discovery_dir/config.err"; then
127
+ if ! automation_resolve_opencode_config "$discovery_dir/config.json" "$discovery_dir/config.err"; then
128
128
  automation_die "OpenCode resolved config failed; see $discovery_dir/config.err"
129
129
  fi
130
130
  superpowers_pin="$(jq -r '.plugins.superpowers' "$AUTOMATION_CONFIG")"
@@ -16,15 +16,15 @@ if [[ -z "$task_id" || -z "$expected" || "$separator" != "--" || -z "$filter" ||
16
16
  fi
17
17
 
18
18
  automation_validate_task_id "$task_id"
19
- safe_filter_pattern='^[A-Za-z0-9_.#$*-]+$'
20
- [[ "$filter" =~ $safe_filter_pattern ]] || automation_die "unsafe test filter: $filter"
19
+ automation_validate_test_filter "$filter"
21
20
  [[ ${#expected} -ge 3 ]] || automation_die "expected failure text is too short"
22
21
  [[ "$(automation_read_state "$task_id")" == "CODING" ]] || automation_die "$task_id is not CODING"
23
22
  "$SCRIPT_DIR/validate-contract.sh" "$task_id"
24
23
 
25
24
  contract="$(automation_contract_path "$task_id")"
26
- jq -e --arg filter "$filter" '.targetTests | index($filter) != null' "$contract" >/dev/null || \
27
- automation_die "test filter is not declared in the contract: $filter"
25
+ target_count="$(jq -er --arg filter "$filter" '[.targetTests[] | select(.filter == $filter)] | length' "$contract")"
26
+ [[ "$target_count" -eq 1 ]] || automation_die "test filter must identify exactly one declared contract target: $filter"
27
+ gradle_task="$(jq -er --arg filter "$filter" '.targetTests[] | select(.filter == $filter) | .gradleTask' "$contract")"
28
28
 
29
29
  evidence_dir="$(automation_evidence_path "$task_id")"
30
30
  mkdir -p "$evidence_dir"
@@ -34,10 +34,7 @@ red_meta="$evidence_dir/red.json"
34
34
  started_at="$(automation_now)"
35
35
 
36
36
  set +e
37
- (
38
- cd "$AUTOMATION_ROOT"
39
- ./gradlew testDebugUnitTest --tests "$filter"
40
- ) 2>&1 | tee "$red_log"
37
+ automation_run_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT" 2>&1 | tee "$red_log"
41
38
  red_status=${PIPESTATUS[0]}
42
39
  set -e
43
40
 
@@ -49,9 +46,10 @@ jq -n \
49
46
  --arg startedAt "$started_at" \
50
47
  --arg finishedAt "$(automation_now)" \
51
48
  --arg expectedFailure "$expected" \
49
+ --arg gradleTask "$gradle_task" \
52
50
  --arg testFilter "$filter" \
53
51
  --argjson exitCode "$red_status" \
54
- '{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt, command: ["./gradlew", "testDebugUnitTest", "--tests", $testFilter], expectedFailure: $expectedFailure, exitCode: $exitCode}' \
52
+ '{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt, command: ["./gradlew", $gradleTask, "--tests", $testFilter], expectedFailure: $expectedFailure, exitCode: $exitCode}' \
55
53
  | automation_record_json "$red_meta"
56
54
 
57
55
  automation_info "$task_id RED evidence recorded"
@@ -85,7 +85,7 @@ printf -- '- Reviewer:`%s`,独立复验退出码 `%s`\n' \
85
85
  "$(jq -r '.evidence.reviewerVerificationExitCode' "$report_file")"
86
86
  printf -- '- Reviewer 摘要:%s\n' "$(jq -r '.reviewSummary' "$report_file")"
87
87
  printf -- '- 聚焦测试:\n'
88
- jq -r '.targetTests[] | " - `\(.)`"' "$report_file"
88
+ jq -r '.targetTests[] | " - `./gradlew \(.gradleTask) --tests \(.filter)`"' "$report_file"
89
89
 
90
90
  printf '\n### P1 · 绑定与剩余风险\n\n'
91
91
  printf -- '- 当前 diff 与质量门、独立 Review、验收包的 SHA 三方一致。\n'