@frankzhang2026/opencode-android-orchestrator 0.9.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.
- package/CHANGELOG.md +34 -0
- package/README.md +92 -55
- package/dist/cli.js +14 -1
- package/dist/cli.js.map +1 -1
- package/dist/config/android-sdk.d.ts +10 -0
- package/dist/config/android-sdk.d.ts.map +1 -0
- package/dist/config/android-sdk.js +50 -0
- package/dist/config/android-sdk.js.map +1 -0
- package/dist/config/commit-message-prefix.d.ts +31 -0
- package/dist/config/commit-message-prefix.d.ts.map +1 -0
- package/dist/config/commit-message-prefix.js +155 -0
- package/dist/config/commit-message-prefix.js.map +1 -0
- package/dist/config/queue-policy.d.ts +12 -0
- package/dist/config/queue-policy.d.ts.map +1 -0
- package/dist/config/queue-policy.js +18 -0
- package/dist/config/queue-policy.js.map +1 -0
- package/dist/config/verification-policy.d.ts +10 -3
- package/dist/config/verification-policy.d.ts.map +1 -1
- package/dist/config/verification-policy.js +66 -13
- package/dist/config/verification-policy.js.map +1 -1
- package/dist/doctor/index.d.ts.map +1 -1
- package/dist/doctor/index.js +3 -49
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/installation.d.ts.map +1 -1
- package/dist/doctor/installation.js +81 -0
- package/dist/doctor/installation.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/installer/adaptive-templates.d.ts +8 -2
- package/dist/installer/adaptive-templates.d.ts.map +1 -1
- package/dist/installer/adaptive-templates.js +10 -0
- package/dist/installer/adaptive-templates.js.map +1 -1
- package/dist/installer/index.d.ts +1 -0
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +1 -0
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/init.d.ts +4 -1
- package/dist/installer/init.d.ts.map +1 -1
- package/dist/installer/init.js +60 -9
- package/dist/installer/init.js.map +1 -1
- package/dist/installer/install-manifest.d.ts +1 -0
- package/dist/installer/install-manifest.d.ts.map +1 -1
- package/dist/installer/install-manifest.js +6 -1
- package/dist/installer/install-manifest.js.map +1 -1
- package/dist/installer/opencode-config.d.ts +3 -3
- package/dist/installer/opencode-config.js +1 -1
- package/dist/installer/uninstall.d.ts.map +1 -1
- package/dist/installer/uninstall.js +5 -0
- package/dist/installer/uninstall.js.map +1 -1
- package/dist/installer/upgrade.d.ts +5 -1
- package/dist/installer/upgrade.d.ts.map +1 -1
- package/dist/installer/upgrade.js +94 -11
- package/dist/installer/upgrade.js.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11 -5
- package/dist/plugin/index.js.map +1 -1
- package/dist/queue/approvals.d.ts +49 -0
- package/dist/queue/approvals.d.ts.map +1 -0
- package/dist/queue/approvals.js +68 -0
- package/dist/queue/approvals.js.map +1 -0
- package/dist/queue/cli.d.ts +3 -0
- package/dist/queue/cli.d.ts.map +1 -0
- package/dist/queue/cli.js +109 -0
- package/dist/queue/cli.js.map +1 -0
- package/dist/queue/executor.d.ts +24 -0
- package/dist/queue/executor.d.ts.map +1 -0
- package/dist/queue/executor.js +445 -0
- package/dist/queue/executor.js.map +1 -0
- package/dist/queue/lifecycle.d.ts +3 -0
- package/dist/queue/lifecycle.d.ts.map +1 -0
- package/dist/queue/lifecycle.js +24 -0
- package/dist/queue/lifecycle.js.map +1 -0
- package/dist/queue/queue.d.ts +154 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +411 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/queue/service.d.ts +15 -0
- package/dist/queue/service.d.ts.map +1 -0
- package/dist/queue/service.js +152 -0
- package/dist/queue/service.js.map +1 -0
- package/dist/queue/storage.d.ts +30 -0
- package/dist/queue/storage.d.ts.map +1 -0
- package/dist/queue/storage.js +171 -0
- package/dist/queue/storage.js.map +1 -0
- package/dist/queue/tools.d.ts +12 -0
- package/dist/queue/tools.d.ts.map +1 -0
- package/dist/queue/tools.js +144 -0
- package/dist/queue/tools.js.map +1 -0
- package/docs/MIGRATION.md +47 -14
- package/docs/QUEUE.md +146 -0
- package/docs/SECURITY.md +48 -32
- package/docs/TROUBLESHOOTING.md +36 -11
- package/package.json +1 -1
- package/templates/.opencode/agents/scheduled-planner.md +60 -131
- package/templates/.opencode/commands/abort-task.md +6 -15
- package/templates/.opencode/commands/acceptance.md +6 -15
- package/templates/.opencode/commands/change.md +3 -1
- package/templates/.opencode/commands/resume-review.md +6 -14
- package/templates/.opencode/commands/resume-task.md +6 -23
- package/templates/.opencode/skills/scheduled-quality-orchestrator/SKILL.md +69 -248
- package/templates/AGENTS.md.fragment +8 -0
- package/templates/README.md +9 -1
- package/templates/automation/config.json +9 -2
- package/templates/automation/config.schema.json +222 -45
- package/templates/scripts/automation/abort-task.sh +4 -2
- package/templates/scripts/automation/accept-and-integrate.sh +9 -2
- package/templates/scripts/automation/acceptance-report.sh +4 -2
- package/templates/scripts/automation/approve-and-run.sh +5 -0
- package/templates/scripts/automation/begin-review.sh +1 -0
- package/templates/scripts/automation/block-task.sh +1 -0
- package/templates/scripts/automation/claim-task.sh +1 -0
- package/templates/scripts/automation/lib.sh +186 -2
- package/templates/scripts/automation/orchestrate-task.sh +12 -0
- package/templates/scripts/automation/preflight.sh +25 -4
- package/templates/scripts/automation/prepare-contract-review.sh +4 -0
- package/templates/scripts/automation/quality-gate.sh +1 -0
- package/templates/scripts/automation/record-red.sh +1 -0
- package/templates/scripts/automation/resume-review-fix.sh +1 -0
- package/templates/scripts/automation/resume-review.sh +1 -0
- package/templates/scripts/automation/resume-task.sh +1 -0
- package/templates/scripts/automation/scope-gate.sh +10 -2
- package/templates/scripts/automation/show-acceptance-review.sh +3 -0
- package/templates/scripts/automation/submit-review.sh +8 -2
- package/templates/scripts/automation/tests/run-tests.sh +59 -4
- package/templates/scripts/automation/verify-task.sh +13 -0
|
@@ -50,6 +50,9 @@ AUTOMATION_WORKSPACES_DIR="$AUTOMATION_RUNTIME_ROOT/workspaces"
|
|
|
50
50
|
AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH=".automation-worktree-allowlist"
|
|
51
51
|
AUTOMATION_WORKTREE_ALLOWLIST_MAX_BYTES=65536
|
|
52
52
|
AUTOMATION_WORKTREE_ALLOWLIST_MAX_ENTRIES=256
|
|
53
|
+
AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH="automation/automation-commit-prefix"
|
|
54
|
+
AUTOMATION_COMMIT_MESSAGE_PREFIX_FILE_MAX_BYTES=4096
|
|
55
|
+
AUTOMATION_COMMIT_MESSAGE_PREFIX_MAX_BYTES=256
|
|
53
56
|
|
|
54
57
|
automation_info() {
|
|
55
58
|
printf '[automation] %s\n' "$*"
|
|
@@ -111,7 +114,9 @@ automation_validate_config() {
|
|
|
111
114
|
length > 0 and
|
|
112
115
|
length == (unique | length) and
|
|
113
116
|
all(.[]; gradle_task);
|
|
114
|
-
.schemaVersion ==
|
|
117
|
+
(.schemaVersion == 5 or .schemaVersion == 6) and
|
|
118
|
+
((.commitPolicy // "humanApproval") == "humanApproval" or .commitPolicy == "autoCommit") and
|
|
119
|
+
(.workspaceStrategy != "isolatedWorktree" or (.commitPolicy // "humanApproval") == "humanApproval") and
|
|
115
120
|
(.enabled | type == "boolean") and
|
|
116
121
|
(.mode == "shadow" or .mode == "orchestrated") and
|
|
117
122
|
(.workspaceStrategy == "inPlaceExclusive" or .workspaceStrategy == "isolatedWorktree") and
|
|
@@ -122,6 +127,7 @@ automation_validate_config() {
|
|
|
122
127
|
(.maxReviewerRestarts | type == "number" and . >= 0 and . <= 3 and floor == .) and
|
|
123
128
|
(.unitTestsEnabled | type == "boolean") and
|
|
124
129
|
(.lintEnabled | type == "boolean") and
|
|
130
|
+
(.commitMessagePrefixMode == "required" or .commitMessagePrefixMode == "disabled") and
|
|
125
131
|
(.longCommandTimeoutMs | type == "number" and . >= 120000 and . <= 7200000 and floor == .) and
|
|
126
132
|
(.autoCleanupWorktrees | type == "boolean") and
|
|
127
133
|
.pushAfterAcceptance == false and
|
|
@@ -213,6 +219,26 @@ automation_workspace_path() {
|
|
|
213
219
|
printf '%s/%s.json\n' "$AUTOMATION_WORKSPACES_DIR" "$task_id"
|
|
214
220
|
}
|
|
215
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
|
+
|
|
216
242
|
automation_workspace_strategy() {
|
|
217
243
|
local workspace_file="$1"
|
|
218
244
|
jq -er '.workspaceStrategy // "isolatedWorktree"' "$workspace_file"
|
|
@@ -246,6 +272,92 @@ automation_config_value() {
|
|
|
246
272
|
jq -r "$query" "$AUTOMATION_CONFIG"
|
|
247
273
|
}
|
|
248
274
|
|
|
275
|
+
automation_read_commit_message_prefix_at() {
|
|
276
|
+
local root="$1"
|
|
277
|
+
local config_file="$root/automation/config.json"
|
|
278
|
+
local prefix_file="$root/$AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH"
|
|
279
|
+
local mode byte_count line prefix prefix_bytes
|
|
280
|
+
local active_count=0
|
|
281
|
+
|
|
282
|
+
if [[ -L "$config_file" || ! -f "$config_file" ]]; then
|
|
283
|
+
automation_die "missing or unsafe automation configuration: $config_file"
|
|
284
|
+
return 1
|
|
285
|
+
fi
|
|
286
|
+
mode="$(jq -er '.commitMessagePrefixMode' "$config_file")" || {
|
|
287
|
+
automation_die "commitMessagePrefixMode is missing from $config_file"
|
|
288
|
+
return 1
|
|
289
|
+
}
|
|
290
|
+
case "$mode" in
|
|
291
|
+
disabled) return 0 ;;
|
|
292
|
+
required) ;;
|
|
293
|
+
*)
|
|
294
|
+
automation_die "unsupported commitMessagePrefixMode: $mode"
|
|
295
|
+
return 1
|
|
296
|
+
;;
|
|
297
|
+
esac
|
|
298
|
+
|
|
299
|
+
if [[ ! -e "$prefix_file" && ! -L "$prefix_file" ]]; then
|
|
300
|
+
automation_die "commit-message prefix is required; fill $prefix_file before starting a task"
|
|
301
|
+
return 1
|
|
302
|
+
fi
|
|
303
|
+
if [[ -L "$prefix_file" || ! -f "$prefix_file" ]]; then
|
|
304
|
+
automation_die "commit-message prefix must be a regular file, not a symlink: $prefix_file"
|
|
305
|
+
return 1
|
|
306
|
+
fi
|
|
307
|
+
byte_count="$(wc -c < "$prefix_file" | tr -d '[:space:]')"
|
|
308
|
+
if [[ ! "$byte_count" =~ ^[0-9]+$ ]] || \
|
|
309
|
+
[[ "$byte_count" -gt "$AUTOMATION_COMMIT_MESSAGE_PREFIX_FILE_MAX_BYTES" ]]; then
|
|
310
|
+
automation_die "commit-message prefix file exceeds $AUTOMATION_COMMIT_MESSAGE_PREFIX_FILE_MAX_BYTES bytes: $prefix_file"
|
|
311
|
+
return 1
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
315
|
+
line="${line%$'\r'}"
|
|
316
|
+
[[ -z "$line" || "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
|
|
317
|
+
active_count=$((active_count + 1))
|
|
318
|
+
if [[ "$active_count" -gt 1 ]]; then
|
|
319
|
+
automation_die "commit-message prefix must contain exactly one non-comment line: $prefix_file"
|
|
320
|
+
return 1
|
|
321
|
+
fi
|
|
322
|
+
if [[ "$line" == [[:space:]]* || "$line" == *[[:space:]] ]]; then
|
|
323
|
+
automation_die "commit-message prefix must not have leading or trailing whitespace"
|
|
324
|
+
return 1
|
|
325
|
+
fi
|
|
326
|
+
case "$line" in
|
|
327
|
+
*[[:cntrl:]]*)
|
|
328
|
+
automation_die "commit-message prefix must not contain control characters"
|
|
329
|
+
return 1
|
|
330
|
+
;;
|
|
331
|
+
esac
|
|
332
|
+
prefix="$line"
|
|
333
|
+
done < "$prefix_file"
|
|
334
|
+
|
|
335
|
+
if [[ "$active_count" -eq 0 ]]; then
|
|
336
|
+
automation_die "commit-message prefix is not configured; fill $prefix_file before starting a task"
|
|
337
|
+
return 1
|
|
338
|
+
fi
|
|
339
|
+
prefix_bytes="$(printf '%s' "$prefix" | wc -c | tr -d '[:space:]')"
|
|
340
|
+
if [[ ! "$prefix_bytes" =~ ^[0-9]+$ ]] || \
|
|
341
|
+
[[ "$prefix_bytes" -gt "$AUTOMATION_COMMIT_MESSAGE_PREFIX_MAX_BYTES" ]]; then
|
|
342
|
+
automation_die "commit-message prefix exceeds $AUTOMATION_COMMIT_MESSAGE_PREFIX_MAX_BYTES UTF-8 bytes"
|
|
343
|
+
return 1
|
|
344
|
+
fi
|
|
345
|
+
printf '%s\n' "$prefix"
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
automation_commit_message_at() {
|
|
349
|
+
local root="$1"
|
|
350
|
+
local base_message="$2"
|
|
351
|
+
local prefix
|
|
352
|
+
|
|
353
|
+
prefix="$(automation_read_commit_message_prefix_at "$root")" || return 1
|
|
354
|
+
if [[ -n "$prefix" ]]; then
|
|
355
|
+
printf '%s %s\n' "$prefix" "$base_message"
|
|
356
|
+
else
|
|
357
|
+
printf '%s\n' "$base_message"
|
|
358
|
+
fi
|
|
359
|
+
}
|
|
360
|
+
|
|
249
361
|
automation_validate_gradle_task() {
|
|
250
362
|
local task="${1:-}"
|
|
251
363
|
local safe_task_pattern='^([A-Za-z][A-Za-z0-9_.-]*|(:[A-Za-z0-9_.-]+)+)$'
|
|
@@ -296,12 +408,75 @@ automation_run_gradle_group() {
|
|
|
296
408
|
return 1
|
|
297
409
|
}
|
|
298
410
|
|
|
411
|
+
if [[ "$group" == "fullUnitTestTasks" && -n "${AUTOMATION_QUEUE_RUN_ID:-}" ]]; then
|
|
412
|
+
automation_run_fresh_unit_tests "$root" "${tasks[@]}"
|
|
413
|
+
return $?
|
|
414
|
+
fi
|
|
299
415
|
(
|
|
300
416
|
cd "$root"
|
|
301
417
|
./gradlew "${tasks[@]}"
|
|
302
418
|
)
|
|
303
419
|
}
|
|
304
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
|
+
|
|
305
480
|
automation_run_configured_unit_tests() {
|
|
306
481
|
local contract="${1:-}"
|
|
307
482
|
local root="${2:-$AUTOMATION_ROOT}"
|
|
@@ -309,6 +484,12 @@ automation_run_configured_unit_tests() {
|
|
|
309
484
|
|
|
310
485
|
[[ -n "$contract" && -f "$contract" ]] || automation_die "unit-test contract does not exist: $contract"
|
|
311
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
|
|
312
493
|
if [[ "$(automation_config_value '.unitTestsEnabled')" != "true" ]]; then
|
|
313
494
|
automation_info "skipping ${context}unit tests (unitTestsEnabled=false)"
|
|
314
495
|
return 0
|
|
@@ -453,7 +634,7 @@ automation_validate_worktree_allowlist_entry_at() {
|
|
|
453
634
|
;;
|
|
454
635
|
esac
|
|
455
636
|
case "$path" in
|
|
456
|
-
/*|./*|.|..|.git|.git/*|docs/plans|docs/plans/*|"$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH")
|
|
637
|
+
/*|./*|.|..|.git|.git/*|docs/plans|docs/plans/*|"$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH"|"$AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH")
|
|
457
638
|
automation_die "worktree allowlist entry is reserved or unsafe: $path"
|
|
458
639
|
return 1
|
|
459
640
|
;;
|
|
@@ -608,6 +789,7 @@ automation_worktree_path_is_allowlisted() {
|
|
|
608
789
|
local entry
|
|
609
790
|
|
|
610
791
|
[[ "$path" == "$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH" ]] && return 0
|
|
792
|
+
[[ "$path" == "$AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH" ]] && return 0
|
|
611
793
|
while IFS= read -r entry; do
|
|
612
794
|
[[ -n "$entry" ]] || continue
|
|
613
795
|
[[ "$path" == "$entry" ]] && return 0
|
|
@@ -971,6 +1153,7 @@ automation_transition_allowed() {
|
|
|
971
1153
|
READY_FOR_REVIEW:REVIEWING:orchestrator) return 0 ;;
|
|
972
1154
|
READY_FOR_REVIEW:BLOCKED:orchestrator) return 0 ;;
|
|
973
1155
|
REVIEWING:AWAITING_HUMAN:reviewer) return 0 ;;
|
|
1156
|
+
REVIEWING:READY_TO_COMMIT:reviewer) return 0 ;;
|
|
974
1157
|
REVIEWING:CHANGES_REQUESTED:reviewer) return 0 ;;
|
|
975
1158
|
REVIEWING:BLOCKED:orchestrator) return 0 ;;
|
|
976
1159
|
BLOCKED:REVIEWING:human) return 0 ;;
|
|
@@ -979,6 +1162,7 @@ automation_transition_allowed() {
|
|
|
979
1162
|
AWAITING_HUMAN:INTEGRATING:integrator) return 0 ;;
|
|
980
1163
|
INTEGRATING:COMPLETED:integrator) return 0 ;;
|
|
981
1164
|
INTEGRATING:INTEGRATION_BLOCKED:integrator) return 0 ;;
|
|
1165
|
+
READY_TO_COMMIT:ABORTED:human) return 0 ;;
|
|
982
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 ;;
|
|
983
1167
|
*) return 1 ;;
|
|
984
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")"
|
|
@@ -19,6 +20,7 @@ source_root="$(jq -er '.sourceRoot' "$workspace_file")"
|
|
|
19
20
|
if [[ "$(jq -r '.repositoryLeaseRequired // false' "$workspace_file")" == "true" ]]; then
|
|
20
21
|
automation_require_repository_lease "$task_id" "$source_root" "$workspace_strategy"
|
|
21
22
|
fi
|
|
23
|
+
automation_read_commit_message_prefix_at "$source_root" >/dev/null
|
|
22
24
|
|
|
23
25
|
automation_acquire_run_lock "$task_id"
|
|
24
26
|
trap 'automation_release_run_lock' EXIT
|
|
@@ -62,6 +64,12 @@ run_agent() {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
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
|
|
65
73
|
state="$(automation_read_state "$task_id")"
|
|
66
74
|
case "$state" in
|
|
67
75
|
PENDING|CODING)
|
|
@@ -98,6 +106,10 @@ for _step in 1 2 3 4 5 6 7 8; do
|
|
|
98
106
|
./scripts/automation/resume-review-fix.sh "$task_id"
|
|
99
107
|
) || true
|
|
100
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
|
+
;;
|
|
101
113
|
AWAITING_HUMAN)
|
|
102
114
|
automation_info "$task_id reached AWAITING_HUMAN"
|
|
103
115
|
(
|
|
@@ -47,6 +47,17 @@ mode="$(automation_config_value '.mode')"
|
|
|
47
47
|
|
|
48
48
|
[[ "$enabled" == "true" ]] || fail_or_warn "automation is disabled"
|
|
49
49
|
[[ "$mode" == "orchestrated" ]] || fail_or_warn "automation mode is $mode, not orchestrated"
|
|
50
|
+
prefix_validation_error=""
|
|
51
|
+
prefix_root="$AUTOMATION_ROOT"
|
|
52
|
+
if [[ -n "$task_id" ]]; then
|
|
53
|
+
prefix_workspace_file="$(automation_workspace_path "$task_id")"
|
|
54
|
+
if [[ -f "$prefix_workspace_file" ]]; then
|
|
55
|
+
prefix_root="$(jq -er '.sourceRoot' "$prefix_workspace_file")"
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
if ! prefix_validation_error="$(automation_read_commit_message_prefix_at "$prefix_root" 2>&1 >/dev/null)"; then
|
|
59
|
+
fail_or_warn "$prefix_validation_error"
|
|
60
|
+
fi
|
|
50
61
|
|
|
51
62
|
if [[ "${AUTOMATION_TEST_MODE:-0}" != "1" ]]; then
|
|
52
63
|
[[ -n "${ANDROID_HOME:-}" ]] || fail_or_warn "ANDROID_HOME is not set; configure the Android SDK environment before running OpenCode"
|
|
@@ -55,11 +66,15 @@ if [[ "${AUTOMATION_TEST_MODE:-0}" != "1" ]]; then
|
|
|
55
66
|
fi
|
|
56
67
|
fi
|
|
57
68
|
|
|
58
|
-
|
|
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
|
|
59
74
|
fail_or_warn "Git worktree is dirty"
|
|
60
75
|
fi
|
|
61
76
|
|
|
62
|
-
if [[ "$source_mode" == "1" ]]; then
|
|
77
|
+
if [[ "$source_mode" == "1" && "$intake_mode" != "1" ]]; then
|
|
63
78
|
repository_lease_file="$(automation_repository_lease_dir)/lease.json"
|
|
64
79
|
if [[ -f "$repository_lease_file" ]]; then
|
|
65
80
|
fail_or_warn "repository workspace is leased by $(jq -r '.taskId // "an unknown task"' "$repository_lease_file")"
|
|
@@ -147,7 +162,7 @@ else
|
|
|
147
162
|
def last_rule($permission; $pattern):
|
|
148
163
|
[.permission[] | select(.permission == $permission and .pattern == $pattern) | .action][-1];
|
|
149
164
|
(last_rule("*"; "*") == "deny") and
|
|
150
|
-
(last_rule("edit"; "docs/plans/**") == "allow") and
|
|
165
|
+
(((last_rule("edit"; "docs/plans/**") == "allow") and
|
|
151
166
|
(last_rule("edit"; "automation/tasks/**") == "allow") and
|
|
152
167
|
(last_rule("edit"; "**/src/**") == "deny") and
|
|
153
168
|
(last_rule("bash"; "./scripts/automation/prepare-contract-review.sh *") == "allow") and
|
|
@@ -155,7 +170,13 @@ else
|
|
|
155
170
|
(last_rule("bash"; "./scripts/automation/resume-task.sh *") == "allow") and
|
|
156
171
|
(last_rule("bash"; "./scripts/automation/resume-review.sh *") == "allow") and
|
|
157
172
|
(last_rule("bash"; "./scripts/automation/accept-and-integrate.sh *") == "allow") and
|
|
158
|
-
(last_rule("bash"; "./scripts/automation/abort-task.sh *") == "allow")
|
|
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
|
|
159
180
|
(last_rule("android_orchestrator_status"; "*") == "allow") and
|
|
160
181
|
(last_rule("android_orchestrator_doctor"; "*") == "allow") and
|
|
161
182
|
(last_rule("schedule_job"; "*") == "deny") and
|
|
@@ -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 "use android_orchestrator_intake draft to seal artifacts in the independent inbox"
|
|
19
|
+
exit 1
|
|
20
|
+
}
|
|
17
21
|
automation_require_approval proposal "$approval"
|
|
18
22
|
automation_assert_repository_lease_available "$task_id"
|
|
19
23
|
"$SCRIPT_DIR/validate-contract.sh" "$task_id" >/dev/null
|
|
@@ -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_read_state "$task_id")" == "CODING" ]] || automation_die "$task_id is not CODING"
|
|
12
13
|
"$SCRIPT_DIR/validate-contract.sh" "$task_id" >/dev/null
|
|
13
14
|
|
|
@@ -16,6 +16,7 @@ if [[ -z "$task_id" || -z "$expected" || "$separator" != "--" || -z "$filter" ||
|
|
|
16
16
|
fi
|
|
17
17
|
|
|
18
18
|
automation_validate_task_id "$task_id"
|
|
19
|
+
automation_require_queue_execution "$task_id"
|
|
19
20
|
automation_validate_test_filter "$filter"
|
|
20
21
|
[[ ${#expected} -ge 3 ]] || automation_die "expected failure text is too short"
|
|
21
22
|
[[ "$(automation_read_state "$task_id")" == "CODING" ]] || automation_die "$task_id is not CODING"
|
|
@@ -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")" == "CHANGES_REQUESTED" ]] || automation_die "$task_id is not CHANGES_REQUESTED"
|
|
12
13
|
|
|
13
14
|
workspace_file="$(automation_workspace_path "$task_id")"
|
|
@@ -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")"
|
|
@@ -17,9 +17,17 @@ baseline_meta="$evidence_dir/baseline.json"
|
|
|
17
17
|
|
|
18
18
|
baseline_head="$(jq -er '.head' "$baseline_meta")"
|
|
19
19
|
current_head="$(git -C "$AUTOMATION_ROOT" rev-parse HEAD)"
|
|
20
|
-
[[ "$baseline_head" == "$current_head" ]] || { automation_die "HEAD changed after baseline"; exit 40; }
|
|
21
|
-
|
|
22
20
|
workspace_file="$(automation_workspace_path "$task_id")"
|
|
21
|
+
if [[ "$baseline_head" != "$current_head" ]]; then
|
|
22
|
+
automation_require_queue_execution "$task_id"
|
|
23
|
+
jq -e --arg original "$baseline_head" --arg current "$current_head" \
|
|
24
|
+
'.queueKey != null and .workspaceStrategy == "isolatedWorktree" and
|
|
25
|
+
.originalBaselineHead == $original and .baselineHead == $current and
|
|
26
|
+
.revalidationRunId != null' "$workspace_file" >/dev/null || {
|
|
27
|
+
automation_die "HEAD changed after baseline"
|
|
28
|
+
exit 40
|
|
29
|
+
}
|
|
30
|
+
fi
|
|
23
31
|
planning_commit_policy="$(jq -r '.planningArtifactsCommitPolicy // "legacyCommittedSeparately"' "$workspace_file")"
|
|
24
32
|
if [[ "$planning_commit_policy" == "withProductChanges" ]]; then
|
|
25
33
|
automation_assert_planning_artifacts_sealed "$task_id" "$AUTOMATION_ROOT" || exit 40
|
|
@@ -45,6 +45,8 @@ report_diff_sha="$(jq -er '.sealedDiffSha256' "$report_file")"
|
|
|
45
45
|
changed_count="$(jq -er '.changedPaths | length' "$report_file")"
|
|
46
46
|
max_changed_files="$(jq -er '.maxChangedFiles' "$report_file")"
|
|
47
47
|
device_tests_required="$(jq -r '.deviceTestsRequired' "$report_file")"
|
|
48
|
+
title="$(jq -er '.title' "$report_file")"
|
|
49
|
+
commit_message="$(automation_commit_message_at "$source_root" "Implement $title ($task_id)")"
|
|
48
50
|
|
|
49
51
|
printf '# 🔔 人工验收提醒\n\n'
|
|
50
52
|
printf '自动执行已停在 `AWAITING_HUMAN`。下面内容已重新核对 sealed diff;此时代码、计划与任务合同均尚未提交或集成。\n\n'
|
|
@@ -58,6 +60,7 @@ printf '| 任务分支收尾 | 成功集成后自动删除;失败或阻塞时
|
|
|
58
60
|
printf '| sealed diff SHA | `%s` |\n' "$report_diff_sha"
|
|
59
61
|
printf '| 变更范围 | 实际 %s 个 / 合同上限 %s 个 |\n' "$changed_count" "$max_changed_files"
|
|
60
62
|
printf '| 提交策略 | 代码、测试、计划与任务合同合并为一个提交 |\n'
|
|
63
|
+
printf '| 预计提交信息 | `%s` |\n' "$commit_message"
|
|
61
64
|
printf '| 自动证据 | Baseline ✓ · RED ✓ · G1–G6 ✓ · Reviewer APPROVED ✓ |\n'
|
|
62
65
|
printf '\n## 必须重点复核\n\n'
|
|
63
66
|
printf '### P0 · 真实行为是否满足合同\n\n'
|
|
@@ -16,6 +16,7 @@ if [[ -z "$task_id" || -z "$decision" || -z "$summary" ]]; then
|
|
|
16
16
|
fi
|
|
17
17
|
|
|
18
18
|
automation_validate_task_id "$task_id"
|
|
19
|
+
automation_require_queue_execution "$task_id"
|
|
19
20
|
[[ "$decision" == "APPROVED" || "$decision" == "CHANGES_REQUESTED" ]] || automation_die "invalid review decision: $decision"
|
|
20
21
|
[[ ${#summary} -ge 20 ]] || automation_die "review summary must contain at least 20 characters"
|
|
21
22
|
[[ "$(automation_read_state "$task_id")" == "REVIEWING" ]] || automation_die "$task_id is not REVIEWING"
|
|
@@ -64,8 +65,13 @@ jq -nc \
|
|
|
64
65
|
| automation_append_json "$evidence_dir/reviews.jsonl"
|
|
65
66
|
|
|
66
67
|
if [[ "$decision" == "APPROVED" ]]; then
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
workspace_file="$(automation_workspace_path "$task_id")"
|
|
69
|
+
if [[ -n "${AUTOMATION_QUEUE_RUN_ID:-}" && "$(jq -r '.commitPolicy // "humanApproval"' "$workspace_file")" == "autoCommit" ]]; then
|
|
70
|
+
automation_transition_state "$task_id" "REVIEWING" "READY_TO_COMMIT" "reviewer" "independent review approved; sealed automatic local commit authorization must be verified"
|
|
71
|
+
else
|
|
72
|
+
automation_transition_state "$task_id" "REVIEWING" "AWAITING_HUMAN" "reviewer" "independent review approved; human acceptance required"
|
|
73
|
+
automation_info "$task_id approved and awaiting human acceptance"
|
|
74
|
+
fi
|
|
69
75
|
else
|
|
70
76
|
automation_transition_state "$task_id" "REVIEWING" "CHANGES_REQUESTED" "reviewer" "$summary"
|
|
71
77
|
automation_warn "$task_id requires changes"
|