@frankzhang2026/opencode-android-orchestrator 0.10.0 → 1.0.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 (109) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +45 -51
  3. package/dist/cli.js +14 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config/android-sdk.d.ts +10 -0
  6. package/dist/config/android-sdk.d.ts.map +1 -0
  7. package/dist/config/android-sdk.js +50 -0
  8. package/dist/config/android-sdk.js.map +1 -0
  9. package/dist/config/queue-policy.d.ts +12 -0
  10. package/dist/config/queue-policy.d.ts.map +1 -0
  11. package/dist/config/queue-policy.js +18 -0
  12. package/dist/config/queue-policy.js.map +1 -0
  13. package/dist/config/verification-policy.d.ts +8 -3
  14. package/dist/config/verification-policy.d.ts.map +1 -1
  15. package/dist/config/verification-policy.js +48 -14
  16. package/dist/config/verification-policy.js.map +1 -1
  17. package/dist/doctor/index.d.ts.map +1 -1
  18. package/dist/doctor/index.js +3 -49
  19. package/dist/doctor/index.js.map +1 -1
  20. package/dist/doctor/installation.d.ts.map +1 -1
  21. package/dist/doctor/installation.js +3 -0
  22. package/dist/doctor/installation.js.map +1 -1
  23. package/dist/installer/adaptive-templates.d.ts +3 -1
  24. package/dist/installer/adaptive-templates.d.ts.map +1 -1
  25. package/dist/installer/adaptive-templates.js +2 -0
  26. package/dist/installer/adaptive-templates.js.map +1 -1
  27. package/dist/installer/install-manifest.d.ts +1 -0
  28. package/dist/installer/install-manifest.d.ts.map +1 -1
  29. package/dist/installer/install-manifest.js +6 -1
  30. package/dist/installer/install-manifest.js.map +1 -1
  31. package/dist/installer/opencode-config.d.ts +3 -3
  32. package/dist/installer/opencode-config.d.ts.map +1 -1
  33. package/dist/installer/opencode-config.js +1 -1
  34. package/dist/installer/opencode-config.js.map +1 -1
  35. package/dist/installer/uninstall.d.ts.map +1 -1
  36. package/dist/installer/uninstall.js +5 -0
  37. package/dist/installer/uninstall.js.map +1 -1
  38. package/dist/installer/upgrade.d.ts.map +1 -1
  39. package/dist/installer/upgrade.js +13 -1
  40. package/dist/installer/upgrade.js.map +1 -1
  41. package/dist/plugin/index.d.ts.map +1 -1
  42. package/dist/plugin/index.js +11 -5
  43. package/dist/plugin/index.js.map +1 -1
  44. package/dist/queue/approvals.d.ts +49 -0
  45. package/dist/queue/approvals.d.ts.map +1 -0
  46. package/dist/queue/approvals.js +68 -0
  47. package/dist/queue/approvals.js.map +1 -0
  48. package/dist/queue/cli.d.ts +3 -0
  49. package/dist/queue/cli.d.ts.map +1 -0
  50. package/dist/queue/cli.js +109 -0
  51. package/dist/queue/cli.js.map +1 -0
  52. package/dist/queue/executor.d.ts +24 -0
  53. package/dist/queue/executor.d.ts.map +1 -0
  54. package/dist/queue/executor.js +445 -0
  55. package/dist/queue/executor.js.map +1 -0
  56. package/dist/queue/lifecycle.d.ts +3 -0
  57. package/dist/queue/lifecycle.d.ts.map +1 -0
  58. package/dist/queue/lifecycle.js +24 -0
  59. package/dist/queue/lifecycle.js.map +1 -0
  60. package/dist/queue/queue.d.ts +154 -0
  61. package/dist/queue/queue.d.ts.map +1 -0
  62. package/dist/queue/queue.js +411 -0
  63. package/dist/queue/queue.js.map +1 -0
  64. package/dist/queue/service.d.ts +15 -0
  65. package/dist/queue/service.d.ts.map +1 -0
  66. package/dist/queue/service.js +152 -0
  67. package/dist/queue/service.js.map +1 -0
  68. package/dist/queue/storage.d.ts +30 -0
  69. package/dist/queue/storage.d.ts.map +1 -0
  70. package/dist/queue/storage.js +171 -0
  71. package/dist/queue/storage.js.map +1 -0
  72. package/dist/queue/tools.d.ts +12 -0
  73. package/dist/queue/tools.d.ts.map +1 -0
  74. package/dist/queue/tools.js +144 -0
  75. package/dist/queue/tools.js.map +1 -0
  76. package/docs/MIGRATION.md +31 -11
  77. package/docs/QUEUE.md +146 -0
  78. package/docs/SECURITY.md +38 -31
  79. package/docs/TROUBLESHOOTING.md +34 -11
  80. package/package.json +1 -1
  81. package/templates/.opencode/agents/scheduled-planner.md +60 -131
  82. package/templates/.opencode/commands/abort-task.md +6 -15
  83. package/templates/.opencode/commands/acceptance.md +6 -15
  84. package/templates/.opencode/commands/change.md +3 -1
  85. package/templates/.opencode/commands/resume-review.md +6 -14
  86. package/templates/.opencode/commands/resume-task.md +6 -23
  87. package/templates/.opencode/skills/scheduled-quality-orchestrator/SKILL.md +69 -248
  88. package/templates/AGENTS.md.fragment +8 -0
  89. package/templates/automation/config.json +8 -2
  90. package/templates/automation/config.schema.json +218 -46
  91. package/templates/scripts/automation/abort-task.sh +2 -1
  92. package/templates/scripts/automation/accept-and-integrate.sh +5 -0
  93. package/templates/scripts/automation/acceptance-report.sh +4 -2
  94. package/templates/scripts/automation/approve-and-run.sh +4 -0
  95. package/templates/scripts/automation/begin-review.sh +1 -0
  96. package/templates/scripts/automation/block-task.sh +1 -0
  97. package/templates/scripts/automation/claim-task.sh +1 -0
  98. package/templates/scripts/automation/lib.sh +94 -1
  99. package/templates/scripts/automation/orchestrate-task.sh +11 -0
  100. package/templates/scripts/automation/preflight.sh +14 -4
  101. package/templates/scripts/automation/prepare-contract-review.sh +4 -0
  102. package/templates/scripts/automation/quality-gate.sh +1 -0
  103. package/templates/scripts/automation/record-red.sh +1 -0
  104. package/templates/scripts/automation/resume-review-fix.sh +1 -0
  105. package/templates/scripts/automation/resume-review.sh +1 -0
  106. package/templates/scripts/automation/resume-task.sh +1 -0
  107. package/templates/scripts/automation/scope-gate.sh +10 -2
  108. package/templates/scripts/automation/submit-review.sh +8 -2
  109. package/templates/scripts/automation/verify-task.sh +13 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "urn:frankzhang2026:opencode-android-orchestrator:automation-config:v5",
3
+ "$id": "urn:frankzhang2026:opencode-android-orchestrator:automation-config:v6",
4
4
  "title": "OpenCode task orchestration configuration",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
@@ -11,7 +11,10 @@
11
11
  "pattern": "^(?!/)(?!\\.\\.(?:/|$))(?![A-Za-z]:[\\\\/]).+"
12
12
  },
13
13
  "nullableIdentifier": {
14
- "type": ["string", "null"],
14
+ "type": [
15
+ "string",
16
+ "null"
17
+ ],
15
18
  "minLength": 1
16
19
  },
17
20
  "gradleTask": {
@@ -23,7 +26,9 @@
23
26
  "type": "array",
24
27
  "minItems": 1,
25
28
  "uniqueItems": true,
26
- "items": { "$ref": "#/$defs/gradleTask" }
29
+ "items": {
30
+ "$ref": "#/$defs/gradleTask"
31
+ }
27
32
  }
28
33
  },
29
34
  "required": [
@@ -46,24 +51,63 @@
46
51
  "requiredSkills",
47
52
  "gradleVerification",
48
53
  "androidProject",
49
- "protectedPaths"
54
+ "protectedPaths",
55
+ "commitPolicy",
56
+ "queue"
50
57
  ],
51
58
  "properties": {
52
- "schemaVersion": { "const": 5 },
53
- "enabled": { "type": "boolean" },
54
- "mode": { "enum": ["shadow", "orchestrated"] },
59
+ "schemaVersion": {
60
+ "const": 6
61
+ },
62
+ "enabled": {
63
+ "type": "boolean"
64
+ },
65
+ "mode": {
66
+ "enum": [
67
+ "shadow",
68
+ "orchestrated"
69
+ ]
70
+ },
55
71
  "workspaceStrategy": {
56
- "enum": ["inPlaceExclusive", "isolatedWorktree"]
57
- },
58
- "originalBranchDriftPolicy": { "const": "block" },
59
- "worktreeBase": { "type": "string" },
60
- "maxFixLoops": { "type": "integer", "minimum": 0, "maximum": 1 },
61
- "maxReviewCycles": { "type": "integer", "minimum": 0, "maximum": 2 },
62
- "maxReviewerRestarts": { "type": "integer", "minimum": 0, "maximum": 3 },
63
- "unitTestsEnabled": { "type": "boolean", "default": true },
64
- "lintEnabled": { "type": "boolean", "default": false },
72
+ "enum": [
73
+ "inPlaceExclusive",
74
+ "isolatedWorktree"
75
+ ]
76
+ },
77
+ "originalBranchDriftPolicy": {
78
+ "const": "block"
79
+ },
80
+ "worktreeBase": {
81
+ "type": "string"
82
+ },
83
+ "maxFixLoops": {
84
+ "type": "integer",
85
+ "minimum": 0,
86
+ "maximum": 1
87
+ },
88
+ "maxReviewCycles": {
89
+ "type": "integer",
90
+ "minimum": 0,
91
+ "maximum": 2
92
+ },
93
+ "maxReviewerRestarts": {
94
+ "type": "integer",
95
+ "minimum": 0,
96
+ "maximum": 3
97
+ },
98
+ "unitTestsEnabled": {
99
+ "type": "boolean",
100
+ "default": true
101
+ },
102
+ "lintEnabled": {
103
+ "type": "boolean",
104
+ "default": false
105
+ },
65
106
  "commitMessagePrefixMode": {
66
- "enum": ["required", "disabled"],
107
+ "enum": [
108
+ "required",
109
+ "disabled"
110
+ ],
67
111
  "default": "required"
68
112
  },
69
113
  "longCommandTimeoutMs": {
@@ -71,25 +115,53 @@
71
115
  "minimum": 120000,
72
116
  "maximum": 7200000
73
117
  },
74
- "autoCleanupWorktrees": { "type": "boolean" },
75
- "pushAfterAcceptance": { "const": false },
118
+ "autoCleanupWorktrees": {
119
+ "type": "boolean"
120
+ },
121
+ "pushAfterAcceptance": {
122
+ "const": false
123
+ },
76
124
  "approvalPhrases": {
77
125
  "type": "object",
78
126
  "additionalProperties": false,
79
- "required": ["proposal", "contract", "acceptance", "abort", "resume"],
127
+ "required": [
128
+ "proposal",
129
+ "contract",
130
+ "acceptance",
131
+ "abort",
132
+ "resume"
133
+ ],
80
134
  "properties": {
81
- "proposal": { "type": "string", "minLength": 4 },
82
- "contract": { "type": "string", "minLength": 4 },
83
- "acceptance": { "type": "string", "minLength": 4 },
84
- "abort": { "type": "string", "minLength": 4 },
85
- "resume": { "type": "string", "minLength": 4 }
135
+ "proposal": {
136
+ "type": "string",
137
+ "minLength": 4
138
+ },
139
+ "contract": {
140
+ "type": "string",
141
+ "minLength": 4
142
+ },
143
+ "acceptance": {
144
+ "type": "string",
145
+ "minLength": 4
146
+ },
147
+ "abort": {
148
+ "type": "string",
149
+ "minLength": 4
150
+ },
151
+ "resume": {
152
+ "type": "string",
153
+ "minLength": 4
154
+ }
86
155
  }
87
156
  },
88
157
  "requiredSkills": {
89
158
  "type": "array",
90
159
  "minItems": 6,
91
160
  "uniqueItems": true,
92
- "items": { "type": "string", "minLength": 1 }
161
+ "items": {
162
+ "type": "string",
163
+ "minLength": 1
164
+ }
93
165
  },
94
166
  "gradleVerification": {
95
167
  "type": "object",
@@ -102,11 +174,21 @@
102
174
  "deviceTestTasks"
103
175
  ],
104
176
  "properties": {
105
- "fullUnitTestTasks": { "$ref": "#/$defs/gradleTaskList" },
106
- "focusedTestTasks": { "$ref": "#/$defs/gradleTaskList" },
107
- "assembleTasks": { "$ref": "#/$defs/gradleTaskList" },
108
- "lintTasks": { "$ref": "#/$defs/gradleTaskList" },
109
- "deviceTestTasks": { "$ref": "#/$defs/gradleTaskList" }
177
+ "fullUnitTestTasks": {
178
+ "$ref": "#/$defs/gradleTaskList"
179
+ },
180
+ "focusedTestTasks": {
181
+ "$ref": "#/$defs/gradleTaskList"
182
+ },
183
+ "assembleTasks": {
184
+ "$ref": "#/$defs/gradleTaskList"
185
+ },
186
+ "lintTasks": {
187
+ "$ref": "#/$defs/gradleTaskList"
188
+ },
189
+ "deviceTestTasks": {
190
+ "$ref": "#/$defs/gradleTaskList"
191
+ }
110
192
  }
111
193
  },
112
194
  "androidProject": {
@@ -122,10 +204,26 @@
122
204
  "testPaths"
123
205
  ],
124
206
  "properties": {
125
- "name": { "type": "string", "minLength": 1 },
126
- "gradleDsl": { "enum": ["kotlin", "groovy", "mixed"] },
127
- "settingsFile": { "$ref": "#/$defs/repositoryPath" },
128
- "moduleScope": { "enum": ["all", "primary"] },
207
+ "name": {
208
+ "type": "string",
209
+ "minLength": 1
210
+ },
211
+ "gradleDsl": {
212
+ "enum": [
213
+ "kotlin",
214
+ "groovy",
215
+ "mixed"
216
+ ]
217
+ },
218
+ "settingsFile": {
219
+ "$ref": "#/$defs/repositoryPath"
220
+ },
221
+ "moduleScope": {
222
+ "enum": [
223
+ "all",
224
+ "primary"
225
+ ]
226
+ },
129
227
  "primaryModule": {
130
228
  "type": "string",
131
229
  "pattern": "^:(?:[A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)*)?$"
@@ -150,9 +248,18 @@
150
248
  "type": "string",
151
249
  "pattern": "^:(?:[A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)*)?$"
152
250
  },
153
- "directory": { "$ref": "#/$defs/repositoryPath" },
154
- "buildFile": { "$ref": "#/$defs/repositoryPath" },
155
- "dsl": { "enum": ["kotlin", "groovy"] },
251
+ "directory": {
252
+ "$ref": "#/$defs/repositoryPath"
253
+ },
254
+ "buildFile": {
255
+ "$ref": "#/$defs/repositoryPath"
256
+ },
257
+ "dsl": {
258
+ "enum": [
259
+ "kotlin",
260
+ "groovy"
261
+ ]
262
+ },
156
263
  "type": {
157
264
  "enum": [
158
265
  "application",
@@ -162,8 +269,12 @@
162
269
  "asset-pack"
163
270
  ]
164
271
  },
165
- "namespace": { "$ref": "#/$defs/nullableIdentifier" },
166
- "applicationId": { "$ref": "#/$defs/nullableIdentifier" }
272
+ "namespace": {
273
+ "$ref": "#/$defs/nullableIdentifier"
274
+ },
275
+ "applicationId": {
276
+ "$ref": "#/$defs/nullableIdentifier"
277
+ }
167
278
  }
168
279
  }
169
280
  },
@@ -173,8 +284,12 @@
173
284
  "uniqueItems": true,
174
285
  "items": {
175
286
  "allOf": [
176
- { "$ref": "#/$defs/repositoryPath" },
177
- { "pattern": "(^|/)src/main/\\*\\*$" }
287
+ {
288
+ "$ref": "#/$defs/repositoryPath"
289
+ },
290
+ {
291
+ "pattern": "(^|/)src/main/\\*\\*$"
292
+ }
178
293
  ]
179
294
  }
180
295
  },
@@ -184,8 +299,12 @@
184
299
  "uniqueItems": true,
185
300
  "items": {
186
301
  "allOf": [
187
- { "$ref": "#/$defs/repositoryPath" },
188
- { "pattern": "(^|/)src/(?:test|androidTest)/\\*\\*$" }
302
+ {
303
+ "$ref": "#/$defs/repositoryPath"
304
+ },
305
+ {
306
+ "pattern": "(^|/)src/(?:test|androidTest)/\\*\\*$"
307
+ }
189
308
  ]
190
309
  }
191
310
  }
@@ -195,7 +314,60 @@
195
314
  "type": "array",
196
315
  "minItems": 1,
197
316
  "uniqueItems": true,
198
- "items": { "$ref": "#/$defs/repositoryPath" }
317
+ "items": {
318
+ "$ref": "#/$defs/repositoryPath"
319
+ }
320
+ },
321
+ "commitPolicy": {
322
+ "enum": [
323
+ "humanApproval",
324
+ "autoCommit"
325
+ ],
326
+ "default": "humanApproval"
327
+ },
328
+ "queue": {
329
+ "type": "object",
330
+ "additionalProperties": false,
331
+ "required": [
332
+ "scanIntervalMs",
333
+ "maxWorkspaces",
334
+ "maxWorkspaceBytes"
335
+ ],
336
+ "properties": {
337
+ "scanIntervalMs": {
338
+ "type": "integer",
339
+ "minimum": 1000,
340
+ "maximum": 60000
341
+ },
342
+ "maxWorkspaces": {
343
+ "type": "integer",
344
+ "minimum": 1,
345
+ "maximum": 32
346
+ },
347
+ "maxWorkspaceBytes": {
348
+ "type": "integer",
349
+ "minimum": 1,
350
+ "default": 21474836480
351
+ }
352
+ }
353
+ }
354
+ },
355
+ "allOf": [
356
+ {
357
+ "if": {
358
+ "properties": {
359
+ "workspaceStrategy": {
360
+ "const": "isolatedWorktree"
361
+ }
362
+ }
363
+ },
364
+ "then": {
365
+ "properties": {
366
+ "commitPolicy": {
367
+ "const": "humanApproval"
368
+ }
369
+ }
370
+ }
199
371
  }
200
- }
372
+ ]
201
373
  }
@@ -13,12 +13,13 @@ if [[ "$#" -ne 2 ]]; then
13
13
  fi
14
14
 
15
15
  automation_validate_task_id "$task_id"
16
+ automation_require_queue_execution "$task_id"
16
17
  automation_require_orchestrated
17
18
  automation_require_approval abort "$approval"
18
19
 
19
20
  current_state="$(automation_read_state "$task_id")"
20
21
  case "$current_state" in
21
- PREPARING|PENDING|CODING|READY_FOR_REVIEW|REVIEWING|CHANGES_REQUESTED|AWAITING_HUMAN|BLOCKED|TEST_FAILED|NEEDS_HUMAN|INTEGRATION_BLOCKED) ;;
22
+ PREPARING|PENDING|CODING|READY_FOR_REVIEW|REVIEWING|CHANGES_REQUESTED|AWAITING_HUMAN|READY_TO_COMMIT|BLOCKED|TEST_FAILED|NEEDS_HUMAN|INTEGRATION_BLOCKED) ;;
22
23
  *) automation_die "cannot abort $task_id from state $current_state" ;;
23
24
  esac
24
25
 
@@ -14,6 +14,11 @@ fi
14
14
 
15
15
  automation_validate_task_id "$task_id"
16
16
  automation_require_orchestrated
17
+ queued_workspace="$(automation_workspace_path "$task_id")"
18
+ if [[ -f "$queued_workspace" && -n "$(jq -r '.queueKey // empty' "$queued_workspace")" ]]; then
19
+ automation_die "queued acceptance must request integration through the repository queue"
20
+ exit 1
21
+ fi
17
22
  automation_require_approval acceptance "$approval"
18
23
  [[ "$(automation_read_state "$task_id")" == "AWAITING_HUMAN" ]] || automation_die "$task_id is not awaiting human acceptance"
19
24
  [[ "$(automation_config_value '.pushAfterAcceptance')" == "false" ]] || automation_die "automatic push is forbidden"
@@ -8,7 +8,8 @@ source "$SCRIPT_DIR/lib.sh"
8
8
  task_id="${1:-}"
9
9
  [[ "$#" -eq 1 ]] || { printf 'Usage: %s TASK-ID\n' "$0" >&2; exit 2; }
10
10
  automation_validate_task_id "$task_id"
11
- [[ "$(automation_read_state "$task_id")" == "AWAITING_HUMAN" ]] || automation_die "$task_id is not AWAITING_HUMAN"
11
+ report_state="$(automation_read_state "$task_id")"
12
+ [[ "$report_state" == "AWAITING_HUMAN" || ( "$report_state" == "READY_TO_COMMIT" && -n "${AUTOMATION_QUEUE_RUN_ID:-}" ) ]] || automation_die "$task_id is not ready for acceptance or authorized local commit"
12
13
 
13
14
  contract="$(automation_contract_path "$task_id")"
14
15
  workspace_file="$(automation_workspace_path "$task_id")"
@@ -60,6 +61,7 @@ automation_worktree_patch_at "$AUTOMATION_ROOT" > "$sealed_diff"
60
61
  report_file="$evidence_dir/acceptance-report.json"
61
62
  jq -n \
62
63
  --arg taskId "$task_id" \
64
+ --arg state "$report_state" \
63
65
  --arg title "$(jq -er '.title' "$contract")" \
64
66
  --arg generatedAt "$(automation_now)" \
65
67
  --arg originalBranch "$original_branch" \
@@ -88,7 +90,7 @@ jq -n \
88
90
  --argjson acceptanceCriteria "$(jq -c '.acceptanceCriteria' "$contract")" \
89
91
  --argjson nonGoals "$(jq -c '.nonGoals' "$contract")" \
90
92
  --argjson targetTests "$(jq -c '.targetTests' "$contract")" \
91
- '{taskId: $taskId, title: $title, state: "AWAITING_HUMAN",
93
+ '{taskId: $taskId, title: $title, state: $state,
92
94
  generatedAt: $generatedAt, originalBranch: $originalBranch,
93
95
  originalHeadBeforeContract: $originalHeadBeforeContract,
94
96
  baselineHead: $baselineHead, originalHeadCurrent: $originalHeadCurrent,
@@ -14,6 +14,10 @@ fi
14
14
 
15
15
  automation_validate_task_id "$task_id"
16
16
  automation_require_orchestrated
17
+ [[ "$(automation_config_value '.schemaVersion')" != "6" ]] || {
18
+ automation_die "contract approval now enqueues through android_orchestrator_intake or the queue enqueue CLI"
19
+ exit 1
20
+ }
17
21
  automation_require_approval contract "$approval"
18
22
  "$SCRIPT_DIR/validate-contract.sh" "$task_id" >/dev/null
19
23
  [[ "$(automation_read_state "$task_id")" == "CONTRACT_REVIEW" ]] || automation_die "$task_id is not awaiting contract review"
@@ -8,6 +8,7 @@ source "$SCRIPT_DIR/lib.sh"
8
8
  task_id="${1:-}"
9
9
  [[ "$#" -eq 1 ]] || { printf 'Usage: %s TASK-ID\n' "$0" >&2; exit 2; }
10
10
  automation_validate_task_id "$task_id"
11
+ automation_require_queue_execution "$task_id"
11
12
  [[ "$(automation_read_state "$task_id")" == "READY_FOR_REVIEW" ]] || automation_die "$task_id is not READY_FOR_REVIEW"
12
13
 
13
14
  ready_file="$(automation_evidence_path "$task_id")/ready.json"
@@ -15,6 +15,7 @@ if [[ -z "$task_id" || ${#reason} -lt 12 ]]; then
15
15
  fi
16
16
 
17
17
  automation_validate_task_id "$task_id"
18
+ automation_require_queue_execution "$task_id"
18
19
  current="$(automation_read_state "$task_id")"
19
20
  case "$current" in
20
21
  PENDING|CODING)
@@ -8,6 +8,7 @@ source "$SCRIPT_DIR/lib.sh"
8
8
  task_id="${1:-}"
9
9
  [[ -n "$task_id" ]] || { printf 'Usage: %s TASK-ID\n' "$0" >&2; exit 2; }
10
10
  automation_validate_task_id "$task_id"
11
+ automation_require_queue_execution "$task_id"
11
12
  automation_require_layout
12
13
 
13
14
  [[ "$(automation_read_state "$task_id")" == "PENDING" ]] || automation_die "$task_id is not PENDING"
@@ -114,7 +114,9 @@ automation_validate_config() {
114
114
  length > 0 and
115
115
  length == (unique | length) and
116
116
  all(.[]; gradle_task);
117
- .schemaVersion == 5 and
117
+ (.schemaVersion == 5 or .schemaVersion == 6) and
118
+ ((.commitPolicy // "humanApproval") == "humanApproval" or .commitPolicy == "autoCommit") and
119
+ (.workspaceStrategy != "isolatedWorktree" or (.commitPolicy // "humanApproval") == "humanApproval") and
118
120
  (.enabled | type == "boolean") and
119
121
  (.mode == "shadow" or .mode == "orchestrated") and
120
122
  (.workspaceStrategy == "inPlaceExclusive" or .workspaceStrategy == "isolatedWorktree") and
@@ -217,6 +219,26 @@ automation_workspace_path() {
217
219
  printf '%s/%s.json\n' "$AUTOMATION_WORKSPACES_DIR" "$task_id"
218
220
  }
219
221
 
222
+ automation_require_queue_execution() {
223
+ local task_id="$1"
224
+ local workspace_file queue_key group
225
+ workspace_file="$(automation_workspace_path "$task_id")"
226
+ [[ -f "$workspace_file" ]] || return 0
227
+ queue_key="$(jq -r '.queueKey // empty' "$workspace_file")"
228
+ [[ -n "$queue_key" ]] || return 0
229
+ [[ -n "${AUTOMATION_QUEUE_RUN_ID:-}" ]] || {
230
+ automation_die "queued tasks must use the repository queue for execution, integration and recovery"
231
+ return 1
232
+ }
233
+ group="$(ps -o pgid= -p "$$" | tr -d ' ')"
234
+ jq -e --arg key "$queue_key" --arg run "$AUTOMATION_QUEUE_RUN_ID" --argjson group "$group" \
235
+ '.active.key == $key and .active.id == $run and .active.worker.pid == $group' \
236
+ "$AUTOMATION_RUNTIME_ROOT/inbox/queue.json" >/dev/null || {
237
+ automation_die "current process does not own this task queue execution"
238
+ return 1
239
+ }
240
+ }
241
+
220
242
  automation_workspace_strategy() {
221
243
  local workspace_file="$1"
222
244
  jq -er '.workspaceStrategy // "isolatedWorktree"' "$workspace_file"
@@ -386,12 +408,75 @@ automation_run_gradle_group() {
386
408
  return 1
387
409
  }
388
410
 
411
+ if [[ "$group" == "fullUnitTestTasks" && -n "${AUTOMATION_QUEUE_RUN_ID:-}" ]]; then
412
+ automation_run_fresh_unit_tests "$root" "${tasks[@]}"
413
+ return $?
414
+ fi
389
415
  (
390
416
  cd "$root"
391
417
  ./gradlew "${tasks[@]}"
392
418
  )
393
419
  }
394
420
 
421
+ # Queue execution reruns Test tasks while preserving compilation/build caches.
422
+ # The init script disables only Test output reuse, including FROM-CACHE, and
423
+ # emits actual test counts. No clean task or model polling is involved.
424
+ automation_run_fresh_unit_tests() {
425
+ local root="$1"
426
+ shift
427
+ local init_file log_file result elapsed start
428
+ mkdir -p "$AUTOMATION_RUNTIME_ROOT/gradle"
429
+ init_file="$(mktemp "$AUTOMATION_RUNTIME_ROOT/gradle/fresh-tests.XXXXXX")"
430
+ log_file="${init_file}.log"
431
+ cat > "$init_file" <<'GRADLE'
432
+ gradle.allprojects { project ->
433
+ project.tasks.withType(org.gradle.api.tasks.testing.Test).configureEach { testTask ->
434
+ outputs.upToDateWhen { false }
435
+ outputs.doNotCacheIf('Orchestrator requires fresh unit-test execution') { true }
436
+ if (testTask.hasProperty('dryRun')) testTask.dryRun = false
437
+ afterSuite { descriptor, result ->
438
+ if (descriptor.parent == null) {
439
+ logger.lifecycle("ORCHESTRATOR_TEST_RESULT|${testTask.path}|${result.testCount}|${result.failedTestCount}|${result.skippedTestCount}")
440
+ }
441
+ }
442
+ }
443
+ }
444
+ gradle.taskGraph.whenReady { graph ->
445
+ graph.allTasks.findAll { it instanceof org.gradle.api.tasks.testing.Test }.each { task ->
446
+ task.logger.lifecycle("ORCHESTRATOR_TEST_EXPECTED|${task.path}")
447
+ }
448
+ }
449
+ gradle.taskGraph.afterTask { task, state ->
450
+ if (task instanceof org.gradle.api.tasks.testing.Test && state.noSource) {
451
+ task.logger.lifecycle("ORCHESTRATOR_TEST_NO_SOURCE|${task.path}")
452
+ }
453
+ }
454
+ GRADLE
455
+ start="$(date +%s)"
456
+ set +e
457
+ (cd "$root" && ./gradlew "$@" --no-configuration-cache --console=plain --init-script "$init_file") 2>&1 | tee "$log_file"
458
+ result=${PIPESTATUS[0]}
459
+ set -e
460
+ elapsed=$(( $(date +%s) - start ))
461
+ [[ "$result" -eq 0 ]] || return "$result"
462
+ # A successful build with only cached, skipped or empty suites is not proof
463
+ # that the required regression tests actually executed.
464
+ awk -F '|' '
465
+ /^ORCHESTRATOR_TEST_EXPECTED\|/ { expected[$2] = 1; count++ }
466
+ /^ORCHESTRATOR_TEST_RESULT\|/ { verified[$2] = 1; total += $3 - $5; failed += $4 }
467
+ /^ORCHESTRATOR_TEST_NO_SOURCE\|/ { verified[$2] = 1 }
468
+ END {
469
+ for (task in expected) if (!verified[task]) missing++
470
+ exit !(count > 0 && total > 0 && failed == 0 && missing == 0)
471
+ }' "$log_file" || {
472
+ automation_die "full unit tests produced no fresh successful test results"
473
+ return 1
474
+ }
475
+ AUTOMATION_FULL_TEST_LOG="$log_file"
476
+ AUTOMATION_FULL_TEST_ELAPSED="$elapsed"
477
+ automation_info "fresh full unit tests executed in ${elapsed}s; log: $log_file"
478
+ }
479
+
395
480
  automation_run_configured_unit_tests() {
396
481
  local contract="${1:-}"
397
482
  local root="${2:-$AUTOMATION_ROOT}"
@@ -399,6 +484,12 @@ automation_run_configured_unit_tests() {
399
484
 
400
485
  [[ -n "$contract" && -f "$contract" ]] || automation_die "unit-test contract does not exist: $contract"
401
486
  automation_validate_config || return 1
487
+ if [[ -n "${AUTOMATION_QUEUE_RUN_ID:-}" ]]; then
488
+ [[ "$(automation_config_value '.unitTestsEnabled')" == "true" ]] || {
489
+ automation_die "queued execution requires full unit tests"
490
+ return 1
491
+ }
492
+ fi
402
493
  if [[ "$(automation_config_value '.unitTestsEnabled')" != "true" ]]; then
403
494
  automation_info "skipping ${context}unit tests (unitTestsEnabled=false)"
404
495
  return 0
@@ -1062,6 +1153,7 @@ automation_transition_allowed() {
1062
1153
  READY_FOR_REVIEW:REVIEWING:orchestrator) return 0 ;;
1063
1154
  READY_FOR_REVIEW:BLOCKED:orchestrator) return 0 ;;
1064
1155
  REVIEWING:AWAITING_HUMAN:reviewer) return 0 ;;
1156
+ REVIEWING:READY_TO_COMMIT:reviewer) return 0 ;;
1065
1157
  REVIEWING:CHANGES_REQUESTED:reviewer) return 0 ;;
1066
1158
  REVIEWING:BLOCKED:orchestrator) return 0 ;;
1067
1159
  BLOCKED:REVIEWING:human) return 0 ;;
@@ -1070,6 +1162,7 @@ automation_transition_allowed() {
1070
1162
  AWAITING_HUMAN:INTEGRATING:integrator) return 0 ;;
1071
1163
  INTEGRATING:COMPLETED:integrator) return 0 ;;
1072
1164
  INTEGRATING:INTEGRATION_BLOCKED:integrator) return 0 ;;
1165
+ READY_TO_COMMIT:ABORTED:human) return 0 ;;
1073
1166
  PREPARING:ABORTED:human|PENDING:ABORTED:human|CODING:ABORTED:human|READY_FOR_REVIEW:ABORTED:human|REVIEWING:ABORTED:human|CHANGES_REQUESTED:ABORTED:human|AWAITING_HUMAN:ABORTED:human|BLOCKED:ABORTED:human|TEST_FAILED:ABORTED:human|NEEDS_HUMAN:ABORTED:human|INTEGRATION_BLOCKED:ABORTED:human) return 0 ;;
1074
1167
  *) return 1 ;;
1075
1168
  esac
@@ -8,6 +8,7 @@ source "$SCRIPT_DIR/lib.sh"
8
8
  task_id="${1:-}"
9
9
  [[ "$#" -eq 1 ]] || { printf 'Usage: %s TASK-ID\n' "$0" >&2; exit 2; }
10
10
  automation_validate_task_id "$task_id"
11
+ automation_require_queue_execution "$task_id"
11
12
  automation_require_orchestrated
12
13
 
13
14
  workspace_file="$(automation_workspace_path "$task_id")"
@@ -63,6 +64,12 @@ run_agent() {
63
64
  }
64
65
 
65
66
  for _step in 1 2 3 4 5 6 7 8; do
67
+ if [[ -n "${AUTOMATION_QUEUE_RUN_ID:-}" ]] && jq -e --arg id "$task_id" \
68
+ 'any(.items[]; .taskId == $id and .request.kind == "abort")' \
69
+ "$AUTOMATION_RUNTIME_ROOT/inbox/queue.json" >/dev/null; then
70
+ automation_info "$task_id has an approved abort request; returning at a safe agent boundary"
71
+ exit 0
72
+ fi
66
73
  state="$(automation_read_state "$task_id")"
67
74
  case "$state" in
68
75
  PENDING|CODING)
@@ -99,6 +106,10 @@ for _step in 1 2 3 4 5 6 7 8; do
99
106
  ./scripts/automation/resume-review-fix.sh "$task_id"
100
107
  ) || true
101
108
  ;;
109
+ READY_TO_COMMIT)
110
+ automation_info "$task_id reached READY_TO_COMMIT; queue executor will verify the sealed authorization"
111
+ exit 0
112
+ ;;
102
113
  AWAITING_HUMAN)
103
114
  automation_info "$task_id reached AWAITING_HUMAN"
104
115
  (
@@ -66,11 +66,15 @@ if [[ "${AUTOMATION_TEST_MODE:-0}" != "1" ]]; then
66
66
  fi
67
67
  fi
68
68
 
69
- if [[ -z "$task_id" ]] && ! automation_worktree_is_clean; then
69
+ intake_mode=0
70
+ if [[ "$source_mode" == "1" && "$(automation_config_value '.schemaVersion')" == "6" ]]; then
71
+ intake_mode=1
72
+ fi
73
+ if [[ -z "$task_id" && "$intake_mode" != "1" ]] && ! automation_worktree_is_clean; then
70
74
  fail_or_warn "Git worktree is dirty"
71
75
  fi
72
76
 
73
- if [[ "$source_mode" == "1" ]]; then
77
+ if [[ "$source_mode" == "1" && "$intake_mode" != "1" ]]; then
74
78
  repository_lease_file="$(automation_repository_lease_dir)/lease.json"
75
79
  if [[ -f "$repository_lease_file" ]]; then
76
80
  fail_or_warn "repository workspace is leased by $(jq -r '.taskId // "an unknown task"' "$repository_lease_file")"
@@ -158,7 +162,7 @@ else
158
162
  def last_rule($permission; $pattern):
159
163
  [.permission[] | select(.permission == $permission and .pattern == $pattern) | .action][-1];
160
164
  (last_rule("*"; "*") == "deny") and
161
- (last_rule("edit"; "docs/plans/**") == "allow") and
165
+ (((last_rule("edit"; "docs/plans/**") == "allow") and
162
166
  (last_rule("edit"; "automation/tasks/**") == "allow") and
163
167
  (last_rule("edit"; "**/src/**") == "deny") and
164
168
  (last_rule("bash"; "./scripts/automation/prepare-contract-review.sh *") == "allow") and
@@ -166,7 +170,13 @@ else
166
170
  (last_rule("bash"; "./scripts/automation/resume-task.sh *") == "allow") and
167
171
  (last_rule("bash"; "./scripts/automation/resume-review.sh *") == "allow") and
168
172
  (last_rule("bash"; "./scripts/automation/accept-and-integrate.sh *") == "allow") and
169
- (last_rule("bash"; "./scripts/automation/abort-task.sh *") == "allow") and
173
+ (last_rule("bash"; "./scripts/automation/abort-task.sh *") == "allow")) or
174
+ ((last_rule("read"; "*") == "deny") and
175
+ (last_rule("edit"; "*") == "deny") and
176
+ (last_rule("bash"; "*") == "deny") and
177
+ (last_rule("android_orchestrator_snapshot"; "*") == "allow") and
178
+ (last_rule("android_orchestrator_intake"; "*") == "allow") and
179
+ (last_rule("android_orchestrator_queue"; "*") == "allow"))) and
170
180
  (last_rule("android_orchestrator_status"; "*") == "allow") and
171
181
  (last_rule("android_orchestrator_doctor"; "*") == "allow") and
172
182
  (last_rule("schedule_job"; "*") == "deny") and