@frankzhang2026/opencode-android-orchestrator 0.2.0 → 0.4.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 +30 -1
- package/README.md +85 -43
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/cli.js +127 -10
- package/dist/cli.js.map +1 -1
- package/dist/doctor/installation.d.ts.map +1 -1
- package/dist/doctor/installation.js +29 -4
- package/dist/doctor/installation.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/installer/adaptive-templates.d.ts +25 -3
- package/dist/installer/adaptive-templates.d.ts.map +1 -1
- package/dist/installer/adaptive-templates.js +75 -9
- package/dist/installer/adaptive-templates.js.map +1 -1
- package/dist/installer/index.d.ts +2 -2
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +2 -2
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/init.d.ts +2 -1
- package/dist/installer/init.d.ts.map +1 -1
- package/dist/installer/init.js +11 -2
- package/dist/installer/init.js.map +1 -1
- package/dist/installer/opencode-config.d.ts +4 -3
- package/dist/installer/opencode-config.d.ts.map +1 -1
- package/dist/installer/opencode-config.js +10 -4
- package/dist/installer/opencode-config.js.map +1 -1
- package/dist/installer/upgrade.d.ts +5 -0
- package/dist/installer/upgrade.d.ts.map +1 -1
- package/dist/installer/upgrade.js +44 -10
- package/dist/installer/upgrade.js.map +1 -1
- package/docs/MIGRATION.md +25 -14
- package/docs/SECURITY.md +29 -5
- package/docs/TROUBLESHOOTING.md +25 -6
- package/package.json +1 -1
- package/templates/.opencode/agents/scheduled-coder.md +3 -0
- package/templates/.opencode/agents/scheduled-reviewer.md +5 -4
- package/templates/.opencode/skills/scheduled-quality-coder/SKILL.md +10 -6
- package/templates/.opencode/skills/scheduled-quality-reviewer/SKILL.md +7 -4
- package/templates/AGENTS.md.fragment +4 -0
- package/templates/README.md +5 -2
- package/templates/automation/config.json +18 -0
- package/templates/automation/config.schema.json +31 -0
- package/templates/automation/task-contract.schema.json +22 -1
- package/templates/automation/tasks/TASK-TEMPLATE.json.example +5 -1
- package/templates/scripts/automation/abort-task.sh +5 -10
- package/templates/scripts/automation/accept-and-integrate.sh +1 -1
- package/templates/scripts/automation/acceptance-report.sh +2 -9
- package/templates/scripts/automation/approve-and-run.sh +3 -0
- package/templates/scripts/automation/claim-task.sh +6 -6
- package/templates/scripts/automation/lib.sh +403 -8
- package/templates/scripts/automation/orchestrate-task.sh +1 -1
- package/templates/scripts/automation/preflight.sh +1 -1
- package/templates/scripts/automation/record-red.sh +7 -9
- package/templates/scripts/automation/scope-gate.sh +12 -9
- package/templates/scripts/automation/show-acceptance-review.sh +1 -1
- package/templates/scripts/automation/status.sh +4 -0
- package/templates/scripts/automation/tests/run-tests.sh +164 -9
- package/templates/scripts/automation/validate-contract.sh +16 -5
- package/templates/scripts/automation/verify-integration.sh +12 -11
- package/templates/scripts/automation/verify-task.sh +13 -28
|
@@ -47,6 +47,9 @@ AUTOMATION_STATE_DIR="$AUTOMATION_RUNTIME_ROOT/state"
|
|
|
47
47
|
AUTOMATION_EVIDENCE_DIR="$AUTOMATION_RUNTIME_ROOT/evidence"
|
|
48
48
|
AUTOMATION_LOCKS_DIR="$AUTOMATION_RUNTIME_ROOT/locks"
|
|
49
49
|
AUTOMATION_WORKSPACES_DIR="$AUTOMATION_RUNTIME_ROOT/workspaces"
|
|
50
|
+
AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH=".automation-worktree-allowlist"
|
|
51
|
+
AUTOMATION_WORKTREE_ALLOWLIST_MAX_BYTES=65536
|
|
52
|
+
AUTOMATION_WORKTREE_ALLOWLIST_MAX_ENTRIES=256
|
|
50
53
|
|
|
51
54
|
automation_info() {
|
|
52
55
|
printf '[automation] %s\n' "$*"
|
|
@@ -69,6 +72,23 @@ automation_require_command() {
|
|
|
69
72
|
command -v "$1" >/dev/null 2>&1 || automation_die "required command is missing: $1"
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
automation_resolve_opencode_config() {
|
|
76
|
+
local output_file="$1"
|
|
77
|
+
local error_file="$2"
|
|
78
|
+
local checkpoint_error="Failed to run the query 'PRAGMA wal_checkpoint(PASSIVE)'"
|
|
79
|
+
|
|
80
|
+
if opencode debug config > "$output_file" 2> "$error_file"; then
|
|
81
|
+
return 0
|
|
82
|
+
fi
|
|
83
|
+
if ! rg -F "$checkpoint_error" "$error_file" >/dev/null; then
|
|
84
|
+
return 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
automation_warn "OpenCode database checkpoint failed once; retrying resolved config discovery"
|
|
88
|
+
sleep 1
|
|
89
|
+
opencode debug config > "$output_file" 2> "$error_file"
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
automation_require_layout() {
|
|
73
93
|
[[ -f "$AUTOMATION_CONFIG" ]] || automation_die "missing $AUTOMATION_CONFIG"
|
|
74
94
|
[[ -d "$AUTOMATION_TASKS_DIR" ]] || automation_die "missing $AUTOMATION_TASKS_DIR"
|
|
@@ -83,6 +103,14 @@ automation_validate_config() {
|
|
|
83
103
|
(startswith("/") | not) and
|
|
84
104
|
(test("^[A-Za-z]:[/\\\\]") | not) and
|
|
85
105
|
(test("(^|/)\\.\\.(/|$)") | not);
|
|
106
|
+
def gradle_task:
|
|
107
|
+
type == "string" and
|
|
108
|
+
test("^(?:[A-Za-z][A-Za-z0-9_.-]*|(?::[A-Za-z0-9_.-]+)+)$");
|
|
109
|
+
def gradle_task_list:
|
|
110
|
+
type == "array" and
|
|
111
|
+
length > 0 and
|
|
112
|
+
length == (unique | length) and
|
|
113
|
+
all(.[]; gradle_task);
|
|
86
114
|
.schemaVersion == 3 and
|
|
87
115
|
(.enabled | type == "boolean") and
|
|
88
116
|
(.mode == "shadow" or .mode == "orchestrated") and
|
|
@@ -101,11 +129,26 @@ automation_validate_config() {
|
|
|
101
129
|
(.plugins | type == "object" and keys == ["superpowers"]) and
|
|
102
130
|
(.plugins.superpowers | type == "string" and length > 0) and
|
|
103
131
|
(.requiredSkills | type == "array" and length >= 6) and
|
|
132
|
+
(.gradleVerification as $verification |
|
|
133
|
+
($verification | type == "object") and
|
|
134
|
+
($verification | keys == [
|
|
135
|
+
"assembleTasks",
|
|
136
|
+
"deviceTestTasks",
|
|
137
|
+
"focusedTestTasks",
|
|
138
|
+
"fullUnitTestTasks",
|
|
139
|
+
"lintTasks"
|
|
140
|
+
]) and
|
|
141
|
+
($verification.fullUnitTestTasks | gradle_task_list) and
|
|
142
|
+
($verification.focusedTestTasks | gradle_task_list) and
|
|
143
|
+
($verification.assembleTasks | gradle_task_list) and
|
|
144
|
+
($verification.lintTasks | gradle_task_list) and
|
|
145
|
+
($verification.deviceTestTasks | gradle_task_list)) and
|
|
104
146
|
(.androidProject as $project |
|
|
105
147
|
($project | type == "object") and
|
|
106
148
|
($project.name | type == "string" and length > 0) and
|
|
107
149
|
($project.gradleDsl == "kotlin" or $project.gradleDsl == "groovy" or $project.gradleDsl == "mixed") and
|
|
108
150
|
($project.settingsFile | repository_path) and
|
|
151
|
+
((($project | has("moduleScope")) | not) or $project.moduleScope == "all" or $project.moduleScope == "primary") and
|
|
109
152
|
($project.primaryModule | type == "string" and test("^:(?:[A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)*)?$")) and
|
|
110
153
|
($project.modules | type == "array" and length > 0 and all(.[];
|
|
111
154
|
(.gradlePath | type == "string" and test("^:(?:[A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)*)?$")) and
|
|
@@ -201,6 +244,84 @@ automation_config_value() {
|
|
|
201
244
|
jq -r "$query" "$AUTOMATION_CONFIG"
|
|
202
245
|
}
|
|
203
246
|
|
|
247
|
+
automation_validate_gradle_task() {
|
|
248
|
+
local task="${1:-}"
|
|
249
|
+
local safe_task_pattern='^([A-Za-z][A-Za-z0-9_.-]*|(:[A-Za-z0-9_.-]+)+)$'
|
|
250
|
+
[[ "$task" =~ $safe_task_pattern ]] || automation_die "unsafe Gradle task: ${task:-<empty>}"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
automation_validate_test_filter() {
|
|
254
|
+
local filter="${1:-}"
|
|
255
|
+
local safe_filter_pattern='^[A-Za-z0-9_.#$*-]+$'
|
|
256
|
+
[[ "$filter" =~ $safe_filter_pattern ]] || automation_die "unsafe test filter: ${filter:-<empty>}"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
automation_validate_gradle_group() {
|
|
260
|
+
local group="${1:-}"
|
|
261
|
+
case "$group" in
|
|
262
|
+
fullUnitTestTasks|focusedTestTasks|assembleTasks|lintTasks|deviceTestTasks) ;;
|
|
263
|
+
*)
|
|
264
|
+
automation_die "unknown Gradle verification group: ${group:-<empty>}"
|
|
265
|
+
return 1
|
|
266
|
+
;;
|
|
267
|
+
esac
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
automation_gradle_group_command_json() {
|
|
271
|
+
local group="$1"
|
|
272
|
+
automation_validate_gradle_group "$group" || return 1
|
|
273
|
+
automation_validate_config || return 1
|
|
274
|
+
jq -ce \
|
|
275
|
+
--arg group "$group" \
|
|
276
|
+
'["./gradlew"] + .gradleVerification[$group]' \
|
|
277
|
+
"$AUTOMATION_CONFIG"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
automation_run_gradle_group() {
|
|
281
|
+
local group="$1"
|
|
282
|
+
local root="${2:-$AUTOMATION_ROOT}"
|
|
283
|
+
local task
|
|
284
|
+
local -a tasks=()
|
|
285
|
+
|
|
286
|
+
automation_validate_gradle_group "$group" || return 1
|
|
287
|
+
automation_validate_config || return 1
|
|
288
|
+
while IFS= read -r task; do
|
|
289
|
+
automation_validate_gradle_task "$task" || return 1
|
|
290
|
+
tasks[${#tasks[@]}]="$task"
|
|
291
|
+
done < <(jq -er --arg group "$group" '.gradleVerification[$group][]' "$AUTOMATION_CONFIG")
|
|
292
|
+
[[ "${#tasks[@]}" -gt 0 ]] || {
|
|
293
|
+
automation_die "Gradle verification group is empty: $group"
|
|
294
|
+
return 1
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
(
|
|
298
|
+
cd "$root"
|
|
299
|
+
./gradlew "${tasks[@]}"
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
automation_run_focused_test() {
|
|
304
|
+
local task="$1"
|
|
305
|
+
local filter="$2"
|
|
306
|
+
local root="${3:-$AUTOMATION_ROOT}"
|
|
307
|
+
|
|
308
|
+
automation_validate_config || return 1
|
|
309
|
+
automation_validate_gradle_task "$task" || return 1
|
|
310
|
+
automation_validate_test_filter "$filter" || return 1
|
|
311
|
+
jq -e \
|
|
312
|
+
--arg task "$task" \
|
|
313
|
+
'.gradleVerification.focusedTestTasks | index($task) != null' \
|
|
314
|
+
"$AUTOMATION_CONFIG" >/dev/null || {
|
|
315
|
+
automation_die "focused Gradle task is not allowed by automation/config.json: $task"
|
|
316
|
+
return 1
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
(
|
|
320
|
+
cd "$root"
|
|
321
|
+
./gradlew "$task" --tests "$filter"
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
204
325
|
automation_require_approval() {
|
|
205
326
|
local kind="$1"
|
|
206
327
|
local supplied="$2"
|
|
@@ -265,12 +386,235 @@ automation_array_matches_path() {
|
|
|
265
386
|
return 1
|
|
266
387
|
}
|
|
267
388
|
|
|
389
|
+
automation_validate_worktree_allowlist_entry_at() {
|
|
390
|
+
local root="$1"
|
|
391
|
+
local path="$2"
|
|
392
|
+
local config_file="$root/automation/config.json"
|
|
393
|
+
local protected
|
|
394
|
+
|
|
395
|
+
if [[ -z "$path" ]]; then
|
|
396
|
+
automation_die "worktree allowlist entries must not be empty"
|
|
397
|
+
return 1
|
|
398
|
+
fi
|
|
399
|
+
if [[ "$path" == [[:space:]]* || "$path" == *[[:space:]] ]]; then
|
|
400
|
+
automation_die "worktree allowlist entries must not have leading or trailing whitespace: $path"
|
|
401
|
+
return 1
|
|
402
|
+
fi
|
|
403
|
+
case "$path" in
|
|
404
|
+
*[[:cntrl:]]*|*'\\'*)
|
|
405
|
+
automation_die "worktree allowlist entries must use plain repository-relative paths: $path"
|
|
406
|
+
return 1
|
|
407
|
+
;;
|
|
408
|
+
*'*'*|*'?'*|*'['*|*']'*)
|
|
409
|
+
automation_die "worktree allowlist entries must be exact file paths, not patterns: $path"
|
|
410
|
+
return 1
|
|
411
|
+
;;
|
|
412
|
+
esac
|
|
413
|
+
case "/$path/" in
|
|
414
|
+
*'//'*|*'/./'*|*'/../'*)
|
|
415
|
+
automation_die "worktree allowlist entries must be normalized repository-relative paths: $path"
|
|
416
|
+
return 1
|
|
417
|
+
;;
|
|
418
|
+
esac
|
|
419
|
+
case "$path" in
|
|
420
|
+
/*|./*|.|..|.git|.git/*|docs/plans|docs/plans/*|"$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH")
|
|
421
|
+
automation_die "worktree allowlist entry is reserved or unsafe: $path"
|
|
422
|
+
return 1
|
|
423
|
+
;;
|
|
424
|
+
esac
|
|
425
|
+
if [[ -d "$root/$path" ]]; then
|
|
426
|
+
automation_die "worktree allowlist entries must identify files, not directories: $path"
|
|
427
|
+
return 1
|
|
428
|
+
fi
|
|
429
|
+
|
|
430
|
+
if [[ ! -f "$config_file" ]]; then
|
|
431
|
+
automation_die "missing automation configuration while validating worktree allowlist: $config_file"
|
|
432
|
+
return 1
|
|
433
|
+
fi
|
|
434
|
+
if ! jq -e '.protectedPaths | type == "array" and all(.[]; type == "string" and length > 0)' \
|
|
435
|
+
"$config_file" >/dev/null; then
|
|
436
|
+
automation_die "automation protectedPaths are invalid while validating the worktree allowlist"
|
|
437
|
+
return 1
|
|
438
|
+
fi
|
|
439
|
+
while IFS= read -r protected; do
|
|
440
|
+
if automation_path_matches "$path" "$protected"; then
|
|
441
|
+
automation_die "worktree allowlist must not include a protected path: $path"
|
|
442
|
+
return 1
|
|
443
|
+
fi
|
|
444
|
+
done < <(jq -r '.protectedPaths[]' "$config_file")
|
|
445
|
+
return 0
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
automation_worktree_allowlist_file_entries_at() {
|
|
449
|
+
local root="$1"
|
|
450
|
+
local allowlist_file="$root/$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH"
|
|
451
|
+
local byte_count line existing
|
|
452
|
+
local entry_count=0
|
|
453
|
+
local -a entries=()
|
|
454
|
+
|
|
455
|
+
if [[ ! -e "$allowlist_file" && ! -L "$allowlist_file" ]]; then
|
|
456
|
+
return 0
|
|
457
|
+
fi
|
|
458
|
+
if [[ -L "$allowlist_file" || ! -f "$allowlist_file" ]]; then
|
|
459
|
+
automation_die "worktree allowlist must be a regular file, not a symlink: $allowlist_file"
|
|
460
|
+
return 1
|
|
461
|
+
fi
|
|
462
|
+
byte_count="$(wc -c < "$allowlist_file" | tr -d '[:space:]')"
|
|
463
|
+
if [[ ! "$byte_count" =~ ^[0-9]+$ ]] || \
|
|
464
|
+
[[ "$byte_count" -gt "$AUTOMATION_WORKTREE_ALLOWLIST_MAX_BYTES" ]]; then
|
|
465
|
+
automation_die "worktree allowlist exceeds $AUTOMATION_WORKTREE_ALLOWLIST_MAX_BYTES bytes: $allowlist_file"
|
|
466
|
+
return 1
|
|
467
|
+
fi
|
|
468
|
+
|
|
469
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
470
|
+
line="${line%$'\r'}"
|
|
471
|
+
[[ -n "$line" && "$line" != \#* ]] || continue
|
|
472
|
+
automation_validate_worktree_allowlist_entry_at "$root" "$line" || return 1
|
|
473
|
+
if [[ "$entry_count" -gt 0 ]]; then
|
|
474
|
+
for existing in "${entries[@]}"; do
|
|
475
|
+
if [[ "$existing" == "$line" ]]; then
|
|
476
|
+
automation_die "duplicate worktree allowlist entry: $line"
|
|
477
|
+
return 1
|
|
478
|
+
fi
|
|
479
|
+
done
|
|
480
|
+
fi
|
|
481
|
+
entry_count=$((entry_count + 1))
|
|
482
|
+
if [[ "$entry_count" -gt "$AUTOMATION_WORKTREE_ALLOWLIST_MAX_ENTRIES" ]]; then
|
|
483
|
+
automation_die "worktree allowlist exceeds $AUTOMATION_WORKTREE_ALLOWLIST_MAX_ENTRIES entries"
|
|
484
|
+
return 1
|
|
485
|
+
fi
|
|
486
|
+
entries[${#entries[@]}]="$line"
|
|
487
|
+
done < "$allowlist_file"
|
|
488
|
+
|
|
489
|
+
if [[ "$entry_count" -gt 0 ]]; then
|
|
490
|
+
for line in "${entries[@]}"; do
|
|
491
|
+
printf '%s\n' "$line"
|
|
492
|
+
done
|
|
493
|
+
fi
|
|
494
|
+
return 0
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
automation_worktree_allowlist_entries_json() {
|
|
498
|
+
local entries="$1"
|
|
499
|
+
if [[ -z "$entries" ]]; then
|
|
500
|
+
printf '[]\n'
|
|
501
|
+
else
|
|
502
|
+
printf '%s\n' "$entries" | jq -Rsc 'split("\n") | map(select(length > 0))'
|
|
503
|
+
fi
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
automation_worktree_allowlist_file_json_at() {
|
|
507
|
+
local root="$1"
|
|
508
|
+
local entries
|
|
509
|
+
entries="$(automation_worktree_allowlist_file_entries_at "$root")" || return 1
|
|
510
|
+
automation_worktree_allowlist_entries_json "$entries"
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
automation_effective_worktree_allowlist_entries_at() {
|
|
514
|
+
local root="$1"
|
|
515
|
+
local resolved_root
|
|
516
|
+
local lease_file="$AUTOMATION_LOCKS_DIR/repository.workspace.lease/lease.json"
|
|
517
|
+
local task_id workspace_file source_root task_root resolved_source resolved_task entry
|
|
518
|
+
|
|
519
|
+
resolved_root="$(cd "$root" && pwd -P)" || {
|
|
520
|
+
automation_die "unable to resolve worktree root for allowlist: $root"
|
|
521
|
+
return 1
|
|
522
|
+
}
|
|
523
|
+
if [[ -f "$lease_file" ]]; then
|
|
524
|
+
task_id="$(jq -er '.taskId' "$lease_file")" || {
|
|
525
|
+
automation_die "repository lease is invalid while resolving the worktree allowlist"
|
|
526
|
+
return 1
|
|
527
|
+
}
|
|
528
|
+
workspace_file="$(automation_workspace_path "$task_id")"
|
|
529
|
+
if [[ -f "$workspace_file" ]] && jq -e 'has("worktreeAllowlist")' "$workspace_file" >/dev/null; then
|
|
530
|
+
jq -e \
|
|
531
|
+
--argjson max "$AUTOMATION_WORKTREE_ALLOWLIST_MAX_ENTRIES" \
|
|
532
|
+
'.worktreeAllowlist as $entries |
|
|
533
|
+
($entries | type == "array") and
|
|
534
|
+
($entries | length <= $max) and
|
|
535
|
+
($entries | length == (unique | length)) and
|
|
536
|
+
all($entries[]; type == "string" and length > 0)' \
|
|
537
|
+
"$workspace_file" >/dev/null || {
|
|
538
|
+
automation_die "workspace worktreeAllowlist snapshot is invalid: $workspace_file"
|
|
539
|
+
return 1
|
|
540
|
+
}
|
|
541
|
+
source_root="$(jq -er '.sourceRoot' "$workspace_file")" || return 1
|
|
542
|
+
task_root="$(automation_workspace_task_root "$workspace_file")" || return 1
|
|
543
|
+
resolved_source="$(cd "$source_root" 2>/dev/null && pwd -P || true)"
|
|
544
|
+
resolved_task="$(cd "$task_root" 2>/dev/null && pwd -P || true)"
|
|
545
|
+
if [[ "$resolved_root" == "$resolved_source" ]]; then
|
|
546
|
+
while IFS= read -r entry; do
|
|
547
|
+
[[ -n "$entry" ]] || continue
|
|
548
|
+
automation_validate_worktree_allowlist_entry_at "$root" "$entry" || return 1
|
|
549
|
+
printf '%s\n' "$entry"
|
|
550
|
+
done < <(jq -r '.worktreeAllowlist[]' "$workspace_file")
|
|
551
|
+
return 0
|
|
552
|
+
fi
|
|
553
|
+
if [[ "$resolved_root" == "$resolved_task" ]]; then
|
|
554
|
+
return 0
|
|
555
|
+
fi
|
|
556
|
+
fi
|
|
557
|
+
fi
|
|
558
|
+
|
|
559
|
+
automation_worktree_allowlist_file_entries_at "$root"
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
automation_effective_worktree_allowlist_json_at() {
|
|
563
|
+
local root="$1"
|
|
564
|
+
local entries
|
|
565
|
+
entries="$(automation_effective_worktree_allowlist_entries_at "$root")" || return 1
|
|
566
|
+
automation_worktree_allowlist_entries_json "$entries"
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
automation_worktree_path_is_allowlisted() {
|
|
570
|
+
local path="$1"
|
|
571
|
+
local entries="$2"
|
|
572
|
+
local entry
|
|
573
|
+
|
|
574
|
+
[[ "$path" == "$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH" ]] && return 0
|
|
575
|
+
while IFS= read -r entry; do
|
|
576
|
+
[[ -n "$entry" ]] || continue
|
|
577
|
+
[[ "$path" == "$entry" ]] && return 0
|
|
578
|
+
done <<< "$entries"
|
|
579
|
+
return 1
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
automation_filter_worktree_paths_at() {
|
|
583
|
+
local root="$1"
|
|
584
|
+
local paths="$2"
|
|
585
|
+
local allowlist path
|
|
586
|
+
|
|
587
|
+
allowlist="$(automation_effective_worktree_allowlist_entries_at "$root")" || return 1
|
|
588
|
+
while IFS= read -r path; do
|
|
589
|
+
[[ -n "$path" ]] || continue
|
|
590
|
+
if ! automation_worktree_path_is_allowlisted "$path" "$allowlist"; then
|
|
591
|
+
printf '%s\n' "$path"
|
|
592
|
+
fi
|
|
593
|
+
done <<< "$paths"
|
|
594
|
+
return 0
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
automation_tracked_changed_paths_at() {
|
|
598
|
+
local root="$1"
|
|
599
|
+
local paths
|
|
600
|
+
paths="$(git -C "$root" diff --no-renames --name-only HEAD -- | LC_ALL=C sort -u)" || return 1
|
|
601
|
+
automation_filter_worktree_paths_at "$root" "$paths"
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
automation_untracked_paths_at() {
|
|
605
|
+
local root="$1"
|
|
606
|
+
local paths
|
|
607
|
+
paths="$(git -C "$root" ls-files --others --exclude-standard | LC_ALL=C sort -u)" || return 1
|
|
608
|
+
automation_filter_worktree_paths_at "$root" "$paths"
|
|
609
|
+
}
|
|
610
|
+
|
|
268
611
|
automation_changed_paths_at() {
|
|
269
612
|
local root="$1"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
613
|
+
local tracked untracked
|
|
614
|
+
|
|
615
|
+
tracked="$(automation_tracked_changed_paths_at "$root")" || return 1
|
|
616
|
+
untracked="$(automation_untracked_paths_at "$root")" || return 1
|
|
617
|
+
printf '%s\n%s\n' "$tracked" "$untracked" | awk 'NF' | LC_ALL=C sort -u
|
|
274
618
|
}
|
|
275
619
|
|
|
276
620
|
automation_changed_paths() {
|
|
@@ -343,20 +687,71 @@ automation_changed_paths_between() {
|
|
|
343
687
|
|
|
344
688
|
automation_worktree_diff_sha() {
|
|
345
689
|
local root="${1:-$AUTOMATION_ROOT}"
|
|
346
|
-
local path
|
|
690
|
+
local tracked_paths untracked_paths path
|
|
691
|
+
tracked_paths="$(automation_tracked_changed_paths_at "$root")" || return 1
|
|
692
|
+
untracked_paths="$(automation_untracked_paths_at "$root")" || return 1
|
|
347
693
|
{
|
|
348
|
-
|
|
694
|
+
while IFS= read -r path; do
|
|
695
|
+
[[ -n "$path" ]] || continue
|
|
696
|
+
git -C "$root" diff --binary --no-renames HEAD -- "$path"
|
|
697
|
+
done <<< "$tracked_paths"
|
|
349
698
|
while IFS= read -r path; do
|
|
350
699
|
[[ -n "$path" ]] || continue
|
|
351
700
|
printf 'UNTRACKED %s\0' "$path"
|
|
352
701
|
git -C "$root" hash-object -- "$path"
|
|
353
|
-
done
|
|
702
|
+
done <<< "$untracked_paths"
|
|
354
703
|
} | shasum -a 256 | awk '{print $1}'
|
|
355
704
|
}
|
|
356
705
|
|
|
706
|
+
automation_worktree_diff_stat_at() {
|
|
707
|
+
local root="${1:-$AUTOMATION_ROOT}"
|
|
708
|
+
local tracked_paths path
|
|
709
|
+
local -a paths=()
|
|
710
|
+
|
|
711
|
+
tracked_paths="$(automation_tracked_changed_paths_at "$root")" || return 1
|
|
712
|
+
while IFS= read -r path; do
|
|
713
|
+
[[ -n "$path" ]] && paths[${#paths[@]}]="$path"
|
|
714
|
+
done <<< "$tracked_paths"
|
|
715
|
+
if [[ "${#paths[@]}" -gt 0 ]]; then
|
|
716
|
+
git -C "$root" diff --stat --no-renames HEAD -- "${paths[@]}"
|
|
717
|
+
fi
|
|
718
|
+
return 0
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
automation_worktree_patch_at() {
|
|
722
|
+
local root="${1:-$AUTOMATION_ROOT}"
|
|
723
|
+
local tracked_paths untracked_paths path
|
|
724
|
+
|
|
725
|
+
tracked_paths="$(automation_tracked_changed_paths_at "$root")" || return 1
|
|
726
|
+
untracked_paths="$(automation_untracked_paths_at "$root")" || return 1
|
|
727
|
+
while IFS= read -r path; do
|
|
728
|
+
[[ -n "$path" ]] || continue
|
|
729
|
+
git -C "$root" diff --binary --no-renames HEAD -- "$path"
|
|
730
|
+
done <<< "$tracked_paths"
|
|
731
|
+
while IFS= read -r path; do
|
|
732
|
+
[[ -n "$path" ]] || continue
|
|
733
|
+
git -C "$root" diff --binary --no-index -- /dev/null "$root/$path" || [[ "$?" -eq 1 ]]
|
|
734
|
+
done <<< "$untracked_paths"
|
|
735
|
+
return 0
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
automation_worktree_status_at() {
|
|
739
|
+
local root="${1:-$AUTOMATION_ROOT}"
|
|
740
|
+
local changed_paths path
|
|
741
|
+
|
|
742
|
+
changed_paths="$(automation_changed_paths_at "$root")" || return 1
|
|
743
|
+
printf 'branch %s\n' "$(automation_current_branch "$root")"
|
|
744
|
+
while IFS= read -r path; do
|
|
745
|
+
[[ -n "$path" ]] && printf 'changed %s\n' "$path"
|
|
746
|
+
done <<< "$changed_paths"
|
|
747
|
+
return 0
|
|
748
|
+
}
|
|
749
|
+
|
|
357
750
|
automation_worktree_is_clean() {
|
|
358
751
|
local root="${1:-$AUTOMATION_ROOT}"
|
|
359
|
-
|
|
752
|
+
local changed_paths
|
|
753
|
+
changed_paths="$(automation_changed_paths_at "$root")" || return 1
|
|
754
|
+
[[ -z "$changed_paths" ]]
|
|
360
755
|
}
|
|
361
756
|
|
|
362
757
|
automation_create_lock_dir() {
|
|
@@ -86,7 +86,7 @@ for _step in 1 2 3 4 5 6 7 8; do
|
|
|
86
86
|
if [[ -f "$agent_runs" ]]; then
|
|
87
87
|
review_attempt="$(jq -s --argjson cycle "$review_cycle" '[.[] | select(.role == "scheduled-reviewer" and .cycle == $cycle)] | length' "$agent_runs")"
|
|
88
88
|
fi
|
|
89
|
-
reviewer_prompt="Use \$scheduled-quality-reviewer with $task_id. This is fresh, non-interactive, read-only review attempt $review_attempt for sealed review cycle $review_cycle. Work within the step budget. Run exactly ./scripts/automation/status.sh $task_id once; its JSON already includes baseline, RED, ready, latest gate, current diff SHA,
|
|
89
|
+
reviewer_prompt="Use \$scheduled-quality-reviewer with $task_id. This is fresh, non-interactive, read-only review attempt $review_attempt for sealed review cycle $review_cycle. Work within the step budget. Run exactly ./scripts/automation/status.sh $task_id once; its JSON already includes baseline, RED, ready, latest gate, current diff SHA, the sealed-SHA comparison, and runtime.effectiveWorktreeAllowlist, so do not browse the external evidence directory. Then inspect the approved contract and run exactly git diff, excluding the control file and every exact path in that effective list from review findings. Submit exactly one decision. Do not browse generated build reports, list the whole repository, retry denied command variants, or run Gradle commands separately before deciding: submit-review.sh performs and records the focused tests, full unit suite, assembleDebug, and lint for APPROVED. Reserve a tool call for submit-review.sh. Never edit, commit, merge, or push."
|
|
90
90
|
run_agent scheduled-reviewer "$reviewer_prompt" "$review_cycle" "$review_attempt" || true
|
|
91
91
|
if [[ "$(automation_read_state "$task_id")" == "REVIEWING" ]]; then
|
|
92
92
|
automation_transition_state "$task_id" "REVIEWING" "BLOCKED" "orchestrator" "reviewer exited without submitting a decision; sealed diff preserved for reviewer-only recovery"
|
|
@@ -124,7 +124,7 @@ else
|
|
|
124
124
|
discovery_dir="$(mktemp -d "${TMPDIR:-/tmp}/opencode-android-orchestrator-preflight.XXXXXX")"
|
|
125
125
|
trap 'rm -rf "$discovery_dir"' EXIT
|
|
126
126
|
|
|
127
|
-
if !
|
|
127
|
+
if ! automation_resolve_opencode_config "$discovery_dir/config.json" "$discovery_dir/config.err"; then
|
|
128
128
|
automation_die "OpenCode resolved config failed; see $discovery_dir/config.err"
|
|
129
129
|
fi
|
|
130
130
|
superpowers_pin="$(jq -r '.plugins.superpowers' "$AUTOMATION_CONFIG")"
|
|
@@ -16,15 +16,15 @@ if [[ -z "$task_id" || -z "$expected" || "$separator" != "--" || -z "$filter" ||
|
|
|
16
16
|
fi
|
|
17
17
|
|
|
18
18
|
automation_validate_task_id "$task_id"
|
|
19
|
-
|
|
20
|
-
[[ "$filter" =~ $safe_filter_pattern ]] || automation_die "unsafe test filter: $filter"
|
|
19
|
+
automation_validate_test_filter "$filter"
|
|
21
20
|
[[ ${#expected} -ge 3 ]] || automation_die "expected failure text is too short"
|
|
22
21
|
[[ "$(automation_read_state "$task_id")" == "CODING" ]] || automation_die "$task_id is not CODING"
|
|
23
22
|
"$SCRIPT_DIR/validate-contract.sh" "$task_id"
|
|
24
23
|
|
|
25
24
|
contract="$(automation_contract_path "$task_id")"
|
|
26
|
-
jq -
|
|
27
|
-
|
|
25
|
+
target_count="$(jq -er --arg filter "$filter" '[.targetTests[] | select(.filter == $filter)] | length' "$contract")"
|
|
26
|
+
[[ "$target_count" -eq 1 ]] || automation_die "test filter must identify exactly one declared contract target: $filter"
|
|
27
|
+
gradle_task="$(jq -er --arg filter "$filter" '.targetTests[] | select(.filter == $filter) | .gradleTask' "$contract")"
|
|
28
28
|
|
|
29
29
|
evidence_dir="$(automation_evidence_path "$task_id")"
|
|
30
30
|
mkdir -p "$evidence_dir"
|
|
@@ -34,10 +34,7 @@ red_meta="$evidence_dir/red.json"
|
|
|
34
34
|
started_at="$(automation_now)"
|
|
35
35
|
|
|
36
36
|
set +e
|
|
37
|
-
|
|
38
|
-
cd "$AUTOMATION_ROOT"
|
|
39
|
-
./gradlew testDebugUnitTest --tests "$filter"
|
|
40
|
-
) 2>&1 | tee "$red_log"
|
|
37
|
+
automation_run_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT" 2>&1 | tee "$red_log"
|
|
41
38
|
red_status=${PIPESTATUS[0]}
|
|
42
39
|
set -e
|
|
43
40
|
|
|
@@ -49,9 +46,10 @@ jq -n \
|
|
|
49
46
|
--arg startedAt "$started_at" \
|
|
50
47
|
--arg finishedAt "$(automation_now)" \
|
|
51
48
|
--arg expectedFailure "$expected" \
|
|
49
|
+
--arg gradleTask "$gradle_task" \
|
|
52
50
|
--arg testFilter "$filter" \
|
|
53
51
|
--argjson exitCode "$red_status" \
|
|
54
|
-
'{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt, command: ["./gradlew",
|
|
52
|
+
'{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt, command: ["./gradlew", $gradleTask, "--tests", $testFilter], expectedFailure: $expectedFailure, exitCode: $exitCode}' \
|
|
55
53
|
| automation_record_json "$red_meta"
|
|
56
54
|
|
|
57
55
|
automation_info "$task_id RED evidence recorded"
|
|
@@ -64,12 +64,13 @@ while IFS= read -r path; do
|
|
|
64
64
|
done <<< "$changed_file_list"
|
|
65
65
|
|
|
66
66
|
deleted_tests="$(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
while IFS= read -r path; do
|
|
68
|
+
[[ -n "$path" ]] || continue
|
|
69
|
+
if [[ -n "$(git -C "$AUTOMATION_ROOT" diff --no-renames --name-only --diff-filter=D HEAD -- "$path")" ]] && \
|
|
70
|
+
automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$path"; then
|
|
71
|
+
printf '%s\n' "$path"
|
|
72
|
+
fi
|
|
73
|
+
done <<< "$changed_file_list"
|
|
73
74
|
)"
|
|
74
75
|
[[ -z "$deleted_tests" ]] || { automation_die "test deletion is forbidden: $deleted_tests"; exit 40; }
|
|
75
76
|
|
|
@@ -83,7 +84,7 @@ added_test_lines="$(
|
|
|
83
84
|
while IFS= read -r path; do
|
|
84
85
|
[[ -n "$path" ]] || continue
|
|
85
86
|
if automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$path"; then
|
|
86
|
-
git -C "$AUTOMATION_ROOT" diff --unified=0 HEAD -- "$path"
|
|
87
|
+
git -C "$AUTOMATION_ROOT" diff --no-renames --unified=0 HEAD -- "$path"
|
|
87
88
|
fi
|
|
88
89
|
done <<< "$changed_file_list" \
|
|
89
90
|
| awk '/^\+\+\+/ { next } /^\+/ { sub(/^\+/, ""); print }'
|
|
@@ -95,12 +96,14 @@ if printf '%s\n' "$added_test_lines" | rg -n "$weakening_pattern" >/dev/null; th
|
|
|
95
96
|
fi
|
|
96
97
|
|
|
97
98
|
while IFS= read -r untracked_test; do
|
|
98
|
-
|
|
99
|
+
[[ -n "$untracked_test" ]] || continue
|
|
100
|
+
if ! git -C "$AUTOMATION_ROOT" ls-files --error-unmatch -- "$untracked_test" >/dev/null 2>&1 && \
|
|
101
|
+
automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$untracked_test" && \
|
|
99
102
|
rg -n "$weakening_pattern" "$AUTOMATION_ROOT/$untracked_test" >/dev/null; then
|
|
100
103
|
automation_die "potential test weakening detected in new test: $untracked_test"
|
|
101
104
|
exit 40
|
|
102
105
|
fi
|
|
103
|
-
done
|
|
106
|
+
done <<< "$changed_file_list"
|
|
104
107
|
|
|
105
108
|
if [[ "$planning_commit_policy" == "withProductChanges" ]]; then
|
|
106
109
|
automation_info "scope gate passed ($changed_count product files; sealed planning artifacts excluded from the contract limit)"
|
|
@@ -85,7 +85,7 @@ printf -- '- Reviewer:`%s`,独立复验退出码 `%s`\n' \
|
|
|
85
85
|
"$(jq -r '.evidence.reviewerVerificationExitCode' "$report_file")"
|
|
86
86
|
printf -- '- Reviewer 摘要:%s\n' "$(jq -r '.reviewSummary' "$report_file")"
|
|
87
87
|
printf -- '- 聚焦测试:\n'
|
|
88
|
-
jq -r '.targetTests[] | " -
|
|
88
|
+
jq -r '.targetTests[] | " - `./gradlew \(.gradleTask) --tests \(.filter)`"' "$report_file"
|
|
89
89
|
|
|
90
90
|
printf '\n### P1 · 绑定与剩余风险\n\n'
|
|
91
91
|
printf -- '- 当前 diff 与质量门、独立 Review、验收包的 SHA 三方一致。\n'
|
|
@@ -33,6 +33,7 @@ if [[ "$ready_json" != "null" ]]; then
|
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
35
|
current_diff_sha=""
|
|
36
|
+
effective_worktree_allowlist_json='[]'
|
|
36
37
|
lease_json=null
|
|
37
38
|
lease_matches=null
|
|
38
39
|
original_head_current=""
|
|
@@ -41,6 +42,7 @@ task_branch_exists=null
|
|
|
41
42
|
if [[ "$workspace_json" != "null" ]]; then
|
|
42
43
|
task_root="$(jq -r '.taskRoot // .taskWorktree // empty' <<< "$workspace_json")"
|
|
43
44
|
if [[ -n "$task_root" && -d "$task_root" ]]; then
|
|
45
|
+
effective_worktree_allowlist_json="$(automation_effective_worktree_allowlist_json_at "$task_root")"
|
|
44
46
|
current_diff_sha="$(automation_worktree_diff_sha "$task_root")"
|
|
45
47
|
fi
|
|
46
48
|
source_root="$(jq -r '.sourceRoot // empty' <<< "$workspace_json")"
|
|
@@ -97,6 +99,7 @@ jq -n \
|
|
|
97
99
|
--argjson repositoryLease "$lease_json" \
|
|
98
100
|
--argjson repositoryLeaseMatches "$lease_matches" \
|
|
99
101
|
--argjson taskBranchExists "$task_branch_exists" \
|
|
102
|
+
--argjson effectiveWorktreeAllowlist "$effective_worktree_allowlist_json" \
|
|
100
103
|
--arg originalHeadCurrent "$original_head_current" \
|
|
101
104
|
--argjson originalBranchDrifted "$original_branch_drifted" \
|
|
102
105
|
--arg evidence "$evidence_dir" \
|
|
@@ -106,6 +109,7 @@ jq -n \
|
|
|
106
109
|
repositoryLease: $repositoryLease,
|
|
107
110
|
repositoryLeaseMatches: $repositoryLeaseMatches,
|
|
108
111
|
taskBranchExists: $taskBranchExists,
|
|
112
|
+
effectiveWorktreeAllowlist: $effectiveWorktreeAllowlist,
|
|
109
113
|
originalHeadCurrent: ($originalHeadCurrent | if length == 0 then null else . end),
|
|
110
114
|
originalBranchDrifted: $originalBranchDrifted
|
|
111
115
|
},
|