@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
package/docs/MIGRATION.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Migration guide
2
2
 
3
3
  This guide covers migration to
4
- `@frankzhang2026/opencode-android-orchestrator@1.0.3`. Pin the exact version and
4
+ `@frankzhang2026/opencode-android-orchestrator@1.0.5`. Pin the exact version and
5
5
  prove the migration in a disposable clone before changing a long-lived
6
6
  repository.
7
7
 
@@ -9,10 +9,10 @@ repository.
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@1.0.3 init .` | Normal new installation; all runtime-detected Android modules and registered debug verification tasks are discovered automatically. |
12
+ | No orchestrator files or manifest | `npx @frankzhang2026/opencode-android-orchestrator@1.0.5 init .` | Normal new installation; all runtime-detected Android modules and registered debug verification tasks are discovered automatically. |
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
- | `0.2.0` through `0.10.0` manifest-managed installation with intact managed/backup content | Run the `1.0.0` `upgrade`; add `--refresh-gradle-discovery` when generated module/task lists are incomplete. | Refresh replaces all derived module metadata, source paths, protected build files, and task allowlists from one Gradle runtime snapshot. Module scope, operator policies, user-owned AGENTS content, and an existing commit-prefix sidecar remain preserved. |
15
- | Healthy `1.0.0` through `1.0.2` installation | Stop the queue service, finish or abort retained workspaces, then run the fixed `1.0.3` `upgrade`. | Pending inbox contracts remain durable. Version 1.0.3 makes omitted task commit policies inherit the repository configuration while preserving the legacy human-approval fallback when the field is absent. |
14
+ | `0.2.0` through `0.10.0` manifest-managed installation with intact managed/backup content | Run the `1.0.5` `upgrade`; add `--refresh-gradle-discovery` when generated module/task lists are incomplete. | Refresh replaces all derived module metadata, source paths, protected build files, and task allowlists from one Gradle runtime snapshot. Module scope, operator policies, user-owned AGENTS content, and an existing commit-prefix sidecar remain preserved. |
15
+ | Healthy `1.0.0` through `1.0.4` installation | Stop the queue service, finish or abort retained workspaces, then run the fixed `1.0.5` `upgrade`. | Pending inbox contracts remain durable. Version 1.0.5 adds schema V3 contracts and rejects RED evidence containing preserved-behavior or undeclared failures; older approved contracts retain their legacy flow. |
16
16
  | 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. |
17
17
  | Healthy older manifest-managed installation | Run `doctor`, then the fixed target version's `upgrade`. | `upgrade` requires a valid installed manifest and intact original backups. |
18
18
  | 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. |
@@ -58,7 +58,7 @@ installation of `0.1.0` alone does not require project-file cleanup.
58
58
  After release, initialize with the fixed version:
59
59
 
60
60
  ```sh
61
- npx @frankzhang2026/opencode-android-orchestrator@1.0.3 init .
61
+ npx @frankzhang2026/opencode-android-orchestrator@1.0.5 init .
62
62
  ```
63
63
 
64
64
  New installations default to all-module scope, so multiple application modules
@@ -106,7 +106,7 @@ Use the lifecycle command selected by the active manifest:
106
106
 
107
107
  ```sh
108
108
  npx --yes --registry=https://registry.npmjs.org/ \
109
- @frankzhang2026/opencode-android-orchestrator@1.0.3 upgrade . --json
109
+ @frankzhang2026/opencode-android-orchestrator@1.0.5 upgrade . --json
110
110
  ```
111
111
 
112
112
  The command-level Registry option is useful when a company-wide npm Registry
@@ -136,7 +136,7 @@ computed includes dynamically or a company convention plugin applied
136
136
 
137
137
  ```sh
138
138
  npx --yes --registry=https://registry.npmjs.org/ \
139
- @frankzhang2026/opencode-android-orchestrator@1.0.3 upgrade . \
139
+ @frankzhang2026/opencode-android-orchestrator@1.0.5 upgrade . \
140
140
  --refresh-gradle-discovery --json
141
141
  ```
142
142
 
@@ -220,7 +220,7 @@ use the queue rather than the old direct Shell commands. See [Queue operation](Q
220
220
  Run all checks from the detected Git root:
221
221
 
222
222
  ```sh
223
- npx @frankzhang2026/opencode-android-orchestrator@1.0.3 doctor .
223
+ npx @frankzhang2026/opencode-android-orchestrator@1.0.5 doctor .
224
224
  opencode debug config
225
225
  opencode debug skill
226
226
  opencode debug agent scheduled-planner
package/docs/QUEUE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Queue and background execution
2
2
 
3
- Version `1.0.3` stores proposals and approved contracts under
3
+ Version `1.0.5` stores proposals and approved contracts under
4
4
  `<git-common-dir>/automation-runtime/inbox/queue.json`. A contract is runnable
5
5
  only after its full plan, version, digest, target branch and commit policy are
6
6
  approved and durably recorded. Planning reads a fixed `planningHead`, so another
@@ -51,9 +51,14 @@ rejected even if it copies the visible run ID.
51
51
 
52
52
  ## Service and scheduling
53
53
 
54
- The first enqueue starts the package-owned detached service. Closing the
55
- Planner does not stop it. Enqueue notifications, deadlines, worker completion,
56
- startup recovery and periodic scans all use the same atomic reservation logic.
54
+ Each newly approved contract atomically clears the queue's pause flag when it
55
+ is enqueued, then starts or wakes the package-owned detached service. This
56
+ resumes the entire queue; existing tasks keep their normal priority/FIFO order,
57
+ deadlines and dependencies. Failed or duplicate enqueue requests do not clear
58
+ the pause flag, and enqueue never clears a fault or replaces an active executor.
59
+ Closing the Planner does not stop the service. Enqueue notifications, deadlines,
60
+ worker completion, startup recovery and periodic scans all use the same atomic
61
+ reservation logic.
57
62
  Idle scans do not call a model. No launchd or external Scheduler is registered.
58
63
  The machine must be awake; a restarted service scans overdue entries once
59
64
  through normal arbitration. Recurring task-template generation is not exposed
@@ -80,7 +85,8 @@ opencode-android-orchestrator queue start .
80
85
  An unsuccessful OpenCode agent process pauses consumption as a shared execution
81
86
  fault; inspect its log and fix provider/environment failures before clearing it.
82
87
 
83
- Pause stops new reservations. Stop terminates the scheduler while preserving
88
+ Pause stops new reservations until explicit resume or a new contract is
89
+ successfully enqueued. Stop terminates the scheduler while preserving
84
90
  its active detached worker. Resume does not clear a fault. Notifications are
85
91
  persisted until acknowledged, so the original Planner session need not remain
86
92
  open. `queue --help` lists direct local-operator commands; interactive agents
@@ -95,8 +101,10 @@ opencode-android-orchestrator queue pause .
95
101
  opencode-android-orchestrator queue policy . isolatedWorktree humanApproval
96
102
  ```
97
103
 
98
- Review and commit the changed `automation/config.json` before resuming. The
99
- queued task fixes its workspace strategy when claimed. Its commit authorization
104
+ Avoid approving new contracts during this maintenance pause: a new enqueue
105
+ resumes queue consumption. Review and commit the changed
106
+ `automation/config.json` before resuming. The queued task fixes its workspace
107
+ strategy when claimed. Its commit authorization
100
108
  remains exactly the approved choice: changing a default cannot grant autoCommit
101
109
  to an older task. Direct configuration drift while a workspace is retained
102
110
  blocks scheduling; restore the recorded strategy before recovery.
@@ -116,6 +124,26 @@ It does not run `clean` or rerun all dependency tasks. Coder, Reviewer and local
116
124
  integration perform the configured full suite and build gates. Evidence records
117
125
  fresh-test logs, configured tasks and elapsed seconds.
118
126
 
127
+ Newly generated contracts use task-contract schema V3. Every verification case
128
+ has a stable ID, a one-based acceptance-criterion reference, an evidence source,
129
+ an exact test identity, and a pre-change classification:
130
+
131
+ - `preserve` must pass before and after implementation;
132
+ - `change` must fail before implementation with the approved exception type and
133
+ optional message fragment, then pass afterwards;
134
+ - `observe` may record a previously uncertain boundary, but a failure is
135
+ accepted only when the contract declares its expected cause.
136
+
137
+ Before product code changes, `record-red.sh TASK-ID` forces fresh focused Test
138
+ execution and collects structured case results through a temporary Gradle init
139
+ script. Missing, duplicate, skipped or undeclared cases, build failures, and any
140
+ preserved behavior failure reject the preflight. Failed attempts remain under
141
+ the task evidence directory; only a completely valid attempt creates immutable
142
+ RED and test-manifest records. Those records bind the contract hash, execution
143
+ baseline and test-only diff, so changing a RED test afterwards invalidates the
144
+ quality gate. V1/V2 contracts already approved before upgrade retain the legacy
145
+ explicit failure-text command.
146
+
119
147
  ## Baselines and recovery
120
148
 
121
149
  Before execution, changes to contract-relevant files or execution configuration
package/docs/SECURITY.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Security model
2
2
 
3
3
  This document describes the security properties of
4
- `@frankzhang2026/opencode-android-orchestrator@1.0.3`. The lifecycle foundation
4
+ `@frankzhang2026/opencode-android-orchestrator@1.0.5`. The lifecycle foundation
5
5
  completed the real OpenCode `1.14.22` and `1.15.13` release matrix in `0.2.0`;
6
6
  `1.0.0` retains that compatibility boundary.
7
7
 
@@ -1,7 +1,7 @@
1
1
  # Troubleshooting
2
2
 
3
3
  Use this guide for
4
- `@frankzhang2026/opencode-android-orchestrator@1.0.3`.
4
+ `@frankzhang2026/opencode-android-orchestrator@1.0.5`.
5
5
 
6
6
  ## Start with read-only evidence
7
7
 
@@ -11,7 +11,7 @@ From the repository root, capture:
11
11
  git status --short --branch
12
12
  git rev-parse HEAD
13
13
  opencode --version
14
- npx @frankzhang2026/opencode-android-orchestrator@1.0.3 doctor . --json
14
+ npx @frankzhang2026/opencode-android-orchestrator@1.0.5 doctor . --json
15
15
  ```
16
16
 
17
17
  If installation never completed, doctor will correctly report a missing or
@@ -39,9 +39,9 @@ command-scoped override:
39
39
 
40
40
  ```sh
41
41
  npm --registry=https://registry.npmjs.org/ view \
42
- @frankzhang2026/opencode-android-orchestrator@1.0.3 version
42
+ @frankzhang2026/opencode-android-orchestrator@1.0.5 version
43
43
  npx --yes --registry=https://registry.npmjs.org/ \
44
- @frankzhang2026/opencode-android-orchestrator@1.0.3 upgrade . --json
44
+ @frankzhang2026/opencode-android-orchestrator@1.0.5 upgrade . --json
45
45
  ```
46
46
 
47
47
  This leaves the company's saved npm configuration unchanged. Use the option
@@ -71,7 +71,7 @@ Git-backed Superpowers plugin at runtime.
71
71
  | Invalid `--long-command-timeout-ms` | The value is not an integer from `120000` through `7200000`. | Use the `1800000` ms default or pass an intentional bounded value to `init`/`upgrade`; do not edit the generated config directly. |
72
72
  | 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`. |
73
73
  | 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. |
74
- | `Bundled Orchestrator skill is unavailable` | The installed `1.0.3` package is incomplete, damaged, or loaded from an unsupported partial copy. | Reinstall the exact package, inspect its `resources/third-party/superpowers-v6.2.0/skills/` entries, restart OpenCode, and rerun `opencode debug skill`. Do not add an external Superpowers plugin as a fallback. |
74
+ | `Bundled Orchestrator skill is unavailable` | The installed `1.0.5` package is incomplete, damaged, or loaded from an unsupported partial copy. | Reinstall the exact package, inspect its `resources/third-party/superpowers-v6.2.0/skills/` entries, restart OpenCode, and rerun `opencode debug skill`. Do not add an external Superpowers plugin as a fallback. |
75
75
  | `current process does not own this task queue execution` immediately after Coder start on 1.0.1 | OpenCode created the tool shell in a separate process group, so 1.0.1 rejected a legitimate Worker descendant. | Upgrade to 1.0.2 or later, restart OpenCode, then use the approved resume or abort workflow for the retained task. Do not edit the queue or lease files. |
76
76
  | The exact Superpowers v6.2.0 plugin remains after upgrade | That entry existed in the verified pre-install OpenCode file and is therefore user-owned. | Leave it in place or remove it as a separate reviewed configuration change. Upgrade only removes the old Orchestrator-managed entry. |
77
77
 
@@ -88,7 +88,7 @@ silence of `./gradlew tasks --all --console=plain | rg ...` in a large build.
88
88
  For an existing installation, run:
89
89
 
90
90
  ```sh
91
- npx @frankzhang2026/opencode-android-orchestrator@1.0.3 upgrade . \
91
+ npx @frankzhang2026/opencode-android-orchestrator@1.0.5 upgrade . \
92
92
  --refresh-gradle-discovery
93
93
  ```
94
94
 
@@ -97,7 +97,7 @@ least `1800000` milliseconds. A higher timeout already supplied by the caller
97
97
  is preserved; unrelated Bash commands are unchanged. To configure one hour,
98
98
  run `upgrade . --long-command-timeout-ms 3600000` on a healthy installation.
99
99
  If a command still reports `120000 ms`, confirm that the project manifest and
100
- OpenCode plugin reference are both `1.0.3`, restart the OpenCode session so the
100
+ OpenCode plugin reference are both `1.0.5`, restart the OpenCode session so the
101
101
  plugin reloads, and rerun doctor before attempting recovery.
102
102
 
103
103
  After installation, inspect OpenCode discovery separately:
@@ -246,6 +246,7 @@ session or a missing notification is not evidence that a task never started.
246
246
  | Symptom | Recovery |
247
247
  | --- | --- |
248
248
  | Fresh matching question receipt required | Show a new queue review/draft question and select it in the same Planner session; do not paste its approval label into chat. |
249
+ | Service is running but the queue is paused | Use `queue resume .` to run existing approvals. A newly approved contract also resumes the queue when enqueued; duplicate or rejected enqueue requests leave the pause unchanged. |
249
250
  | Waiting for human confirmation or fixed workspace | Accept the current candidate or use the approved abort workflow; further contracts may still be enqueued. |
250
251
  | Isolated capacity reached | Integrate or explicitly archive retained workspaces; do not delete failed work simply to advance the queue. |
251
252
  | Execution launch ownership unknown | Stop the recorded launcher, prove it exited, then use `queue recover-execution .`; preserve any partial workspace. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frankzhang2026/opencode-android-orchestrator",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Reusable OpenCode orchestration for Android projects",
5
5
  "license": "MIT",
6
6
  "author": "frankzhang2026",
@@ -109,6 +109,15 @@ literally. Do not infer missing requirements and do not ask questions during a
109
109
  non-interactive run. If anything is ambiguous or blocked, stop and report the exact
110
110
  reason; the deterministic scripts own state transitions.
111
111
 
112
+ For schema V3 tasks, keep production code unchanged while adding the approved
113
+ tests, then call `./scripts/automation/record-red.sh <TASK-ID>` with no model-
114
+ chosen failure text. The script checks every declared case. A familiar exception
115
+ name in a log is not sufficient RED. Fix a test-only preparation error only when
116
+ the contract remains unchanged and its preparation budget allows it; rerun the
117
+ preflight afterwards. Contract contradictions must be blocked with the reported
118
+ case IDs and measured results. Never delete evidence or weaken, skip or reclassify
119
+ a test to pass the preflight.
120
+
112
121
  You may edit only paths allowed both by this agent and by the task contract.
113
122
  Treat `.automation-worktree-allowlist` and the status JSON's
114
123
  `runtime.effectiveWorktreeAllowlist` paths as human-owned local state: never
@@ -62,6 +62,16 @@ approval. After approval, assemble a complete plan and contract in memory and
62
62
  call `android_orchestrator_intake` with action `draft`; do not create files in the
63
63
  product checkout. Preserve the snapshot's target branch and planningHead.
64
64
 
65
+ Use schema V3 structured verification. Give every verification case a stable ID,
66
+ its one-based acceptance-criterion reference, its source, and its exact test
67
+ identity. Classify preserved behavior as `before: pass`, changed behavior as
68
+ `before: fail`, and an uncertain old boundary as `before: observe`. Do not infer
69
+ exact serializer, parser, locale, date or framework output from declarations or
70
+ memory. For preserved behavior, use an existing trusted test or explicitly mark
71
+ the value for baseline capture. Plan examples are implementation guidance and
72
+ must not add requirements beyond the contract. Check acceptance criteria,
73
+ non-goals and verification expectations for contradictions before drafting.
74
+
65
75
  When the draft does not specify a workspace or commit policy, use the values in
66
76
  `automation/config.json`; new installations configure `inPlaceExclusive` and
67
77
  `humanApproval`. A user may explicitly override the commit policy to
@@ -77,7 +87,10 @@ fresh `合同确认` single-choice question using the exact returned `question`
77
87
  key and digest. Adjustments create a new draft version and require a new review.
78
88
 
79
89
  After enqueue succeeds, report its task ID, queue state and policies and return
80
- to the user. The detached repository service owns execution. Never hold this
90
+ to the user. A newly enqueued contract automatically resumes the whole queue
91
+ and starts or wakes its service; existing scheduling rules and faults still
92
+ apply. Failed or duplicate enqueue requests do not clear a pause.
93
+ The detached repository service owns execution. Never hold this
81
94
  conversation waiting for Coder/Reviewer or simulate model polling while idle.
82
95
  The user can immediately submit another request.
83
96
 
@@ -52,7 +52,13 @@ it with `./scripts/automation/block-task.sh <TASK-ID> <reason>` before stopping.
52
52
 
53
53
  4. On the initial coding cycle, add or change the smallest behavior test
54
54
  permitted by `allowedPaths`.
55
- 5. If RED evidence does not already exist, capture a genuine RED result with:
55
+ 5. If RED evidence does not already exist, capture a genuine RED result. For a
56
+ schema V3 contract use:
57
+
58
+ `./scripts/automation/record-red.sh <TASK-ID>`
59
+
60
+ It checks every declared preserved, changed and observed case against fresh
61
+ structured output. For a legacy schema V1/V2 contract use:
56
62
 
57
63
  `./scripts/automation/record-red.sh <TASK-ID> <expected-failure-text> -- <test-filter>`
58
64
 
@@ -39,6 +39,9 @@ description: Plan stable committed code, approve independent inbox contracts, an
39
39
  `{key, digest, approval}`. A new version requires a new approval.
40
40
  7. Report durable enqueue success and return. Execution is asynchronous, one
41
41
  repository slot; the foreground may plan/approve B and C while A runs.
42
+ A newly enqueued contract resumes the whole queue and starts or wakes its
43
+ service. Existing scheduling rules and faults still apply; failed or
44
+ duplicate enqueue requests do not clear a pause.
42
45
 
43
46
  # Acceptance and controls
44
47
 
@@ -38,6 +38,10 @@ you review.
38
38
  5. Check each acceptance criterion against observable behavior. Inspect for
39
39
  regression risk, missing edge cases, out-of-scope changes, test deletion,
40
40
  ignored tests, relaxed assertions, and implementation-shaped tests.
41
+ For schema V3, also verify every structured case source and identity, that
42
+ preserved cases passed before implementation, that changed cases failed only
43
+ for their declared reason, and that RED contains no undeclared failure. Treat
44
+ the Planner and Coder summaries as claims; use the bound preflight evidence.
41
45
  6. Decide independently:
42
46
 
43
47
  - approve only when the diff is correct and evidence is sufficient;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "urn:frankzhang2026:opencode-android-orchestrator:task-contract:v2",
3
+ "$id": "urn:frankzhang2026:opencode-android-orchestrator:task-contract:v3",
4
4
  "title": "Scheduled coding task contract",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
@@ -14,6 +14,63 @@
14
14
  "type": "string",
15
15
  "minLength": 1,
16
16
  "pattern": "^[A-Za-z0-9_.#$*-]+$"
17
+ },
18
+ "expectedFailure": {
19
+ "type": "object",
20
+ "additionalProperties": false,
21
+ "required": ["type", "origin"],
22
+ "properties": {
23
+ "type": { "type": "string", "minLength": 1 },
24
+ "messageIncludes": { "type": "string", "minLength": 3 },
25
+ "origin": { "type": "string", "minLength": 12 }
26
+ }
27
+ },
28
+ "verificationCase": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": ["id", "criterion", "intent", "before", "after", "source", "test"],
32
+ "properties": {
33
+ "id": { "type": "string", "pattern": "^[A-Z][A-Z0-9-]{2,63}$" },
34
+ "criterion": { "type": "integer", "minimum": 1 },
35
+ "intent": { "enum": ["preserve", "change", "observe"] },
36
+ "before": { "enum": ["pass", "fail", "observe"] },
37
+ "after": { "const": "pass" },
38
+ "source": { "enum": ["userRequirement", "existingTest", "baselineCapture", "measuredFact"] },
39
+ "test": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "required": ["target", "className", "name"],
43
+ "properties": {
44
+ "target": { "type": "integer", "minimum": 0 },
45
+ "className": { "type": "string", "minLength": 1 },
46
+ "name": { "type": "string", "minLength": 1 }
47
+ }
48
+ },
49
+ "expectedFailure": { "$ref": "#/$defs/expectedFailure" }
50
+ },
51
+ "allOf": [
52
+ {
53
+ "if": { "properties": { "intent": { "const": "preserve" } } },
54
+ "then": {
55
+ "properties": {
56
+ "before": { "const": "pass" },
57
+ "source": { "enum": ["existingTest", "baselineCapture", "measuredFact"] }
58
+ },
59
+ "not": { "required": ["expectedFailure"] }
60
+ }
61
+ },
62
+ {
63
+ "if": { "properties": { "intent": { "const": "change" } } },
64
+ "then": {
65
+ "properties": { "before": { "const": "fail" }, "source": { "const": "userRequirement" } },
66
+ "required": ["expectedFailure"]
67
+ }
68
+ },
69
+ {
70
+ "if": { "properties": { "intent": { "const": "observe" } } },
71
+ "then": { "properties": { "before": { "const": "observe" } } }
72
+ }
73
+ ]
17
74
  }
18
75
  },
19
76
  "required": [
@@ -32,10 +89,11 @@
32
89
  "nonGoals",
33
90
  "targetTests",
34
91
  "deviceTestsRequired",
35
- "testPolicy"
92
+ "testPolicy",
93
+ "verification"
36
94
  ],
37
95
  "properties": {
38
- "schemaVersion": { "const": 2 },
96
+ "schemaVersion": { "const": 3 },
39
97
  "id": { "type": "string", "pattern": "^TASK-[A-Z0-9-]+$" },
40
98
  "title": { "type": "string", "minLength": 1 },
41
99
  "designApproved": { "const": true },
@@ -87,6 +145,20 @@
87
145
  },
88
146
  "deviceTestsRequired": { "type": "boolean" },
89
147
  "testPolicy": { "enum": ["required", "not-required"] },
90
- "testPolicyReason": { "type": "string" }
148
+ "testPolicyReason": { "type": "string" },
149
+ "verification": {
150
+ "type": "object",
151
+ "additionalProperties": false,
152
+ "required": ["version", "maxPreparationFixes", "cases"],
153
+ "properties": {
154
+ "version": { "const": 1 },
155
+ "maxPreparationFixes": { "type": "integer", "minimum": 0, "maximum": 1 },
156
+ "cases": {
157
+ "type": "array",
158
+ "minItems": 1,
159
+ "items": { "$ref": "#/$defs/verificationCase" }
160
+ }
161
+ }
162
+ }
91
163
  }
92
164
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "schemaVersion": 2,
2
+ "schemaVersion": 3,
3
3
  "id": "TASK-EXAMPLE-001",
4
4
  "title": "Replace with one small, observable behavior change",
5
5
  "designApproved": true,
@@ -53,5 +53,28 @@
53
53
  ],
54
54
  "deviceTestsRequired": false,
55
55
  "testPolicy": "required",
56
- "testPolicyReason": "Behavior changes require a focused regression test"
56
+ "testPolicyReason": "Behavior changes require a focused regression test",
57
+ "verification": {
58
+ "version": 1,
59
+ "maxPreparationFixes": 1,
60
+ "cases": [
61
+ {
62
+ "id": "FOCUSED-BEHAVIOR",
63
+ "criterion": 1,
64
+ "intent": "change",
65
+ "before": "fail",
66
+ "after": "pass",
67
+ "source": "userRequirement",
68
+ "test": {
69
+ "target": 0,
70
+ "className": "ReplaceWithFocusedTest",
71
+ "name": "replace with observable behavior"
72
+ },
73
+ "expectedFailure": {
74
+ "type": "java.lang.AssertionError",
75
+ "origin": "The assertion at the approved behavior call"
76
+ }
77
+ }
78
+ ]
79
+ }
57
80
  }
@@ -26,6 +26,16 @@ red_exit_code="$(jq -er '.exitCode' "$red_file")"
26
26
  review_verification_exit_code="$(jq -er '.verificationExitCode' "$review_file")"
27
27
  [[ "$red_exit_code" -ne 0 ]] || automation_die "RED evidence does not contain a failing test result"
28
28
  [[ "$review_verification_exit_code" -eq 0 ]] || automation_die "independent review verification did not pass"
29
+ structured_red=null
30
+ if [[ "$(jq -er '.schemaVersion' "$contract")" == "3" ]]; then
31
+ preflight_file="$evidence_dir/test-preflight.json"
32
+ manifest_file="$evidence_dir/test-manifest.json"
33
+ [[ -f "$preflight_file" && -f "$manifest_file" ]] || automation_die "structured RED acceptance evidence is incomplete"
34
+ [[ "$(jq -er '.valid' "$preflight_file")" == "true" ]] || automation_die "structured RED preflight was not valid"
35
+ [[ "$(jq -er '.preflightSha256' "$red_file")" == "$(automation_file_sha256 "$preflight_file")" ]] || automation_die "structured RED preflight changed"
36
+ [[ "$(jq -er '.manifestSha256' "$red_file")" == "$(automation_file_sha256 "$manifest_file")" ]] || automation_die "structured RED manifest changed"
37
+ structured_red="$(jq -c '{valid, reasonCode, summary, cases: [.cases[] | {id, criterion, intent, expectedBefore, test, valid}]}' "$preflight_file")"
38
+ fi
29
39
 
30
40
  recorded_task_root="$(automation_workspace_task_root "$workspace_file")"
31
41
  workspace_strategy="$(automation_workspace_strategy "$workspace_file")"
@@ -90,6 +100,7 @@ jq -n \
90
100
  --argjson acceptanceCriteria "$(jq -c '.acceptanceCriteria' "$contract")" \
91
101
  --argjson nonGoals "$(jq -c '.nonGoals' "$contract")" \
92
102
  --argjson targetTests "$(jq -c '.targetTests' "$contract")" \
103
+ --argjson structuredRed "$structured_red" \
93
104
  '{taskId: $taskId, title: $title, state: $state,
94
105
  generatedAt: $generatedAt, originalBranch: $originalBranch,
95
106
  originalHeadBeforeContract: $originalHeadBeforeContract,
@@ -110,6 +121,7 @@ jq -n \
110
121
  baselineRecorded: true,
111
122
  redRecorded: true,
112
123
  redExitCode: $redExitCode,
124
+ structuredRed: $structuredRed,
113
125
  qualityGate: "PASSED",
114
126
  gateAttempts: $gateAttempts,
115
127
  codingCycle: $codingCycle,
@@ -566,6 +566,114 @@ automation_run_focused_test() {
566
566
  )
567
567
  }
568
568
 
569
+ # Run one focused target with fresh Test execution and machine-readable case
570
+ # results. Test assertion failures are collected instead of failing Gradle so
571
+ # the caller can distinguish approved RED from build and fixture failures.
572
+ automation_run_classified_focused_test() {
573
+ local task="$1"
574
+ local filter="$2"
575
+ local root="$3"
576
+ local result_file="$4"
577
+ local log_file="$5"
578
+ local init_file status
579
+
580
+ automation_validate_config || return 1
581
+ automation_validate_gradle_task "$task" || return 1
582
+ automation_validate_test_filter "$filter" || return 1
583
+ jq -e --arg task "$task" \
584
+ '.gradleVerification.focusedTestTasks | index($task) != null' \
585
+ "$AUTOMATION_CONFIG" >/dev/null || {
586
+ automation_die "focused Gradle task is not allowed by automation/config.json: $task"
587
+ return 1
588
+ }
589
+
590
+ mkdir -p "$AUTOMATION_RUNTIME_ROOT/gradle" "$(dirname "$result_file")" "$(dirname "$log_file")"
591
+ init_file="$(mktemp "$AUTOMATION_RUNTIME_ROOT/gradle/classified-tests.XXXXXX")"
592
+ : > "$result_file"
593
+ cat > "$init_file" <<'GRADLE'
594
+ import groovy.json.JsonOutput
595
+ import org.gradle.api.tasks.testing.Test
596
+ import org.gradle.api.tasks.testing.TestDescriptor
597
+ import org.gradle.api.tasks.testing.TestListener
598
+ import org.gradle.api.tasks.testing.TestResult
599
+
600
+ def outputPath = System.getProperty('orchestrator.caseResultFile')
601
+ if (outputPath == null || outputPath.isEmpty()) {
602
+ throw new GradleException('orchestrator.caseResultFile is required')
603
+ }
604
+ def outputFile = new File(outputPath)
605
+ def appendResult = { Map value ->
606
+ synchronized (gradle) {
607
+ outputFile << JsonOutput.toJson(value) << System.lineSeparator()
608
+ }
609
+ }
610
+
611
+ gradle.allprojects { project ->
612
+ project.tasks.withType(Test).configureEach { testTask ->
613
+ outputs.upToDateWhen { false }
614
+ outputs.doNotCacheIf('Orchestrator requires fresh classified test execution') { true }
615
+ ignoreFailures = true
616
+ failFast = false
617
+ if (testTask.hasProperty('dryRun')) testTask.dryRun = false
618
+ addTestListener(new TestListener() {
619
+ void beforeSuite(TestDescriptor descriptor) {}
620
+ void beforeTest(TestDescriptor descriptor) {}
621
+ void afterTest(TestDescriptor descriptor, TestResult result) {
622
+ def failure = result.exceptions == null || result.exceptions.isEmpty() ? null : result.exceptions[0]
623
+ appendResult([
624
+ kind: 'case', taskPath: testTask.path,
625
+ className: descriptor.className ?: '', name: descriptor.name ?: '',
626
+ result: result.resultType.toString(),
627
+ exceptionType: failure == null ? null : failure.class.name,
628
+ exceptionMessage: failure == null ? null : (failure.message ?: '')
629
+ ])
630
+ }
631
+ void afterSuite(TestDescriptor descriptor, TestResult result) {
632
+ if (descriptor.parent == null) {
633
+ appendResult([
634
+ kind: 'suite', taskPath: testTask.path,
635
+ tests: result.testCount, failures: result.failedTestCount,
636
+ skipped: result.skippedTestCount,
637
+ result: result.resultType.toString()
638
+ ])
639
+ }
640
+ }
641
+ })
642
+ }
643
+ }
644
+ GRADLE
645
+
646
+ set +e
647
+ (cd "$root" && ./gradlew "$task" --tests "$filter" \
648
+ --no-configuration-cache --console=plain --init-script "$init_file" \
649
+ "-Dorchestrator.caseResultFile=$result_file") 2>&1 | tee "$log_file"
650
+ status=${PIPESTATUS[0]}
651
+ set -e
652
+ rm -f "$init_file"
653
+ return "$status"
654
+ }
655
+
656
+ automation_test_diff_sha() {
657
+ local task_id="$1"
658
+ local root="${2:-$AUTOMATION_ROOT}"
659
+ local path tracked=0
660
+ {
661
+ while IFS= read -r path; do
662
+ [[ -n "$path" ]] || continue
663
+ if automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$path"; then
664
+ tracked=1
665
+ if git -C "$root" ls-files --error-unmatch -- "$path" >/dev/null 2>&1; then
666
+ git -C "$root" diff --binary --no-renames HEAD -- "$path"
667
+ else
668
+ printf 'UNTRACKED %s\0' "$path"
669
+ git -C "$root" hash-object -- "$path"
670
+ fi
671
+ fi
672
+ done < <(automation_product_changed_paths_at "$task_id" "$root")
673
+ [[ "$tracked" == "1" ]] || printf 'NO-TEST-CHANGES'
674
+ } | shasum -a 256 | awk '{print $1}'
675
+ }
676
+
569
677
  automation_require_approval() {
570
678
  local kind="$1"
571
679
  local supplied="$2"