@frankzhang2026/opencode-android-orchestrator 0.3.0 → 0.5.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 +29 -0
- package/README.md +81 -20
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/installer/android-project.js +1 -1
- package/dist/installer/android-project.js.map +1 -1
- package/dist/installer/gradle-verification.d.ts +23 -0
- package/dist/installer/gradle-verification.d.ts.map +1 -0
- package/dist/installer/gradle-verification.js +231 -0
- package/dist/installer/gradle-verification.js.map +1 -0
- package/dist/installer/index.d.ts +1 -1
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +1 -1
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/init.d.ts +6 -1
- package/dist/installer/init.d.ts.map +1 -1
- package/dist/installer/init.js +144 -19
- package/dist/installer/init.js.map +1 -1
- package/dist/installer/opencode-config.d.ts +3 -3
- package/dist/installer/opencode-config.js +1 -1
- package/docs/MIGRATION.md +11 -11
- package/docs/SECURITY.md +21 -4
- package/docs/TROUBLESHOOTING.md +28 -2
- 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 +16 -0
- package/templates/automation/config.json +1 -0
- package/templates/automation/tasks/TASK-TEMPLATE.json.example +1 -0
- 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/lib.sh +285 -8
- package/templates/scripts/automation/orchestrate-task.sh +1 -1
- package/templates/scripts/automation/scope-gate.sh +12 -9
- package/templates/scripts/automation/status.sh +4 -0
- package/templates/scripts/automation/tests/run-tests.sh +138 -0
|
@@ -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' "$*"
|
|
@@ -383,12 +386,235 @@ automation_array_matches_path() {
|
|
|
383
386
|
return 1
|
|
384
387
|
}
|
|
385
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
|
+
|
|
386
611
|
automation_changed_paths_at() {
|
|
387
612
|
local root="$1"
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
392
618
|
}
|
|
393
619
|
|
|
394
620
|
automation_changed_paths() {
|
|
@@ -461,20 +687,71 @@ automation_changed_paths_between() {
|
|
|
461
687
|
|
|
462
688
|
automation_worktree_diff_sha() {
|
|
463
689
|
local root="${1:-$AUTOMATION_ROOT}"
|
|
464
|
-
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
|
|
465
693
|
{
|
|
466
|
-
|
|
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"
|
|
467
698
|
while IFS= read -r path; do
|
|
468
699
|
[[ -n "$path" ]] || continue
|
|
469
700
|
printf 'UNTRACKED %s\0' "$path"
|
|
470
701
|
git -C "$root" hash-object -- "$path"
|
|
471
|
-
done
|
|
702
|
+
done <<< "$untracked_paths"
|
|
472
703
|
} | shasum -a 256 | awk '{print $1}'
|
|
473
704
|
}
|
|
474
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
|
+
|
|
475
750
|
automation_worktree_is_clean() {
|
|
476
751
|
local root="${1:-$AUTOMATION_ROOT}"
|
|
477
|
-
|
|
752
|
+
local changed_paths
|
|
753
|
+
changed_paths="$(automation_changed_paths_at "$root")" || return 1
|
|
754
|
+
[[ -z "$changed_paths" ]]
|
|
478
755
|
}
|
|
479
756
|
|
|
480
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"
|
|
@@ -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)"
|
|
@@ -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
|
},
|
|
@@ -82,6 +82,7 @@ write_workspace() {
|
|
|
82
82
|
mkdir -p \
|
|
83
83
|
"$fixture/automation/tasks" \
|
|
84
84
|
"$fixture/docs/plans" \
|
|
85
|
+
"$fixture/local" \
|
|
85
86
|
"$fixture/scripts/automation" \
|
|
86
87
|
"$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture" \
|
|
87
88
|
"$fixture/mobile-client/src/test/java/dev/example/orchestratorfixture"
|
|
@@ -89,6 +90,8 @@ mkdir -p \
|
|
|
89
90
|
cp "$SOURCE_SCRIPT_DIR"/*.sh "$fixture/scripts/automation/"
|
|
90
91
|
chmod +x "$fixture/scripts/automation/"*.sh
|
|
91
92
|
printf '%s\n' '# Approved test plan' > "$fixture/docs/plans/TASK-TEST-001.md"
|
|
93
|
+
printf '%s\n' 'operator baseline' > "$fixture/local/operator-note.txt"
|
|
94
|
+
printf '%s\n' 'rename baseline' > "$fixture/local/rename-source.txt"
|
|
92
95
|
printf '%s\n' 'class ExistingFeature' > "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/ExistingFeature.kt"
|
|
93
96
|
|
|
94
97
|
printf '%s\n' \
|
|
@@ -166,6 +169,7 @@ jq -n '{
|
|
|
166
169
|
]
|
|
167
170
|
},
|
|
168
171
|
protectedPaths: [
|
|
172
|
+
".automation-worktree-allowlist",
|
|
169
173
|
".opencode/",
|
|
170
174
|
".automation-plugin/",
|
|
171
175
|
"automation/",
|
|
@@ -200,6 +204,7 @@ jq -n '{
|
|
|
200
204
|
"mobile-client/src/test/java/dev/example/orchestratorfixture/**"
|
|
201
205
|
],
|
|
202
206
|
forbiddenPaths: [
|
|
207
|
+
".automation-worktree-allowlist",
|
|
203
208
|
".opencode/**",
|
|
204
209
|
".automation-plugin/**",
|
|
205
210
|
"automation/**",
|
|
@@ -243,6 +248,74 @@ jq -n '{
|
|
|
243
248
|
git commit -qm 'Create automation fixture'
|
|
244
249
|
)
|
|
245
250
|
|
|
251
|
+
printf '%s\n' \
|
|
252
|
+
'# Exact repository-relative paths intentionally kept local' \
|
|
253
|
+
'local/operator-note.txt' \
|
|
254
|
+
'local/generated-note.txt' \
|
|
255
|
+
> "$fixture/.automation-worktree-allowlist"
|
|
256
|
+
printf '%s\n' 'operator local edit' > "$fixture/local/operator-note.txt"
|
|
257
|
+
printf '%s\n' 'generated local state' > "$fixture/local/generated-note.txt"
|
|
258
|
+
run_fixture ./scripts/automation/preflight.sh --source >/dev/null
|
|
259
|
+
allowlisted_changes="$(run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths')"
|
|
260
|
+
[[ -z "$allowlisted_changes" ]] || fail "allowlisted paths remained visible: $allowlisted_changes"
|
|
261
|
+
|
|
262
|
+
printf '%s\n' \
|
|
263
|
+
'local/operator-note.txt' \
|
|
264
|
+
'local/generated-note.txt' \
|
|
265
|
+
'local/rename-target.txt' \
|
|
266
|
+
> "$fixture/.automation-worktree-allowlist"
|
|
267
|
+
git -C "$fixture" mv local/rename-source.txt local/rename-target.txt
|
|
268
|
+
rename_changes="$(run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths')"
|
|
269
|
+
[[ "$rename_changes" == "local/rename-source.txt" ]] || \
|
|
270
|
+
fail "an allowlisted rename target hid its non-allowlisted source: $rename_changes"
|
|
271
|
+
git -C "$fixture" mv local/rename-target.txt local/rename-source.txt
|
|
272
|
+
printf '%s\n' \
|
|
273
|
+
'# Exact repository-relative paths intentionally kept local' \
|
|
274
|
+
'local/operator-note.txt' \
|
|
275
|
+
'local/generated-note.txt' \
|
|
276
|
+
> "$fixture/.automation-worktree-allowlist"
|
|
277
|
+
pass 'exact tracked and untracked entries are ignored without hiding rename sources'
|
|
278
|
+
|
|
279
|
+
printf '%s\n' 'must still block' > "$fixture/unapproved-change.txt"
|
|
280
|
+
if run_fixture ./scripts/automation/preflight.sh --source >/dev/null 2>&1; then
|
|
281
|
+
fail 'preflight ignored a path that was not allowlisted'
|
|
282
|
+
fi
|
|
283
|
+
rm "$fixture/unapproved-change.txt"
|
|
284
|
+
pass 'a non-allowlisted worktree change still blocks orchestration startup'
|
|
285
|
+
|
|
286
|
+
printf '%s\n' 'automation/config.json' > "$fixture/.automation-worktree-allowlist"
|
|
287
|
+
if run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths >/dev/null' 2>/dev/null; then
|
|
288
|
+
fail 'worktree allowlist accepted a protected automation path'
|
|
289
|
+
fi
|
|
290
|
+
printf '%s\n' 'local/*.json' > "$fixture/.automation-worktree-allowlist"
|
|
291
|
+
if run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths >/dev/null' 2>/dev/null; then
|
|
292
|
+
fail 'worktree allowlist accepted a glob pattern'
|
|
293
|
+
fi
|
|
294
|
+
printf '%s\n' 'local' > "$fixture/.automation-worktree-allowlist"
|
|
295
|
+
if run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths >/dev/null' 2>/dev/null; then
|
|
296
|
+
fail 'worktree allowlist accepted a directory path'
|
|
297
|
+
fi
|
|
298
|
+
printf '%s\n' 'local/operator-note.txt' 'local/operator-note.txt' > "$fixture/.automation-worktree-allowlist"
|
|
299
|
+
if run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths >/dev/null' 2>/dev/null; then
|
|
300
|
+
fail 'worktree allowlist accepted duplicate entries'
|
|
301
|
+
fi
|
|
302
|
+
printf '%s\n' '../outside.txt' > "$fixture/.automation-worktree-allowlist"
|
|
303
|
+
if run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths >/dev/null' 2>/dev/null; then
|
|
304
|
+
fail 'worktree allowlist accepted a path outside the repository'
|
|
305
|
+
fi
|
|
306
|
+
rm "$fixture/.automation-worktree-allowlist"
|
|
307
|
+
ln -s local/operator-note.txt "$fixture/.automation-worktree-allowlist"
|
|
308
|
+
if run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths >/dev/null' 2>/dev/null; then
|
|
309
|
+
fail 'worktree allowlist accepted a symbolic-link control file'
|
|
310
|
+
fi
|
|
311
|
+
rm "$fixture/.automation-worktree-allowlist"
|
|
312
|
+
printf '%s\n' \
|
|
313
|
+
'# Exact repository-relative paths intentionally kept local' \
|
|
314
|
+
'local/operator-note.txt' \
|
|
315
|
+
'local/generated-note.txt' \
|
|
316
|
+
> "$fixture/.automation-worktree-allowlist"
|
|
317
|
+
pass 'invalid, duplicate, symbolic-link, patterned, and protected allowlist input fails closed'
|
|
318
|
+
|
|
246
319
|
run_fixture ./scripts/automation/validate-contract.sh TASK-TEST-001 >/dev/null
|
|
247
320
|
pass 'valid version-3 configuration and task contract are accepted'
|
|
248
321
|
|
|
@@ -500,6 +573,8 @@ approved_baseline="$(jq -er '.baselineHead' "$runtime_root/workspaces/TASK-TEST-
|
|
|
500
573
|
leased_status="$(run_fixture ./scripts/automation/status.sh TASK-TEST-004)"
|
|
501
574
|
[[ "$(jq -r '.runtime.repositoryLeaseMatches' <<< "$leased_status")" == "true" ]] || fail 'status did not verify the repository lease'
|
|
502
575
|
[[ "$(jq -r '.runtime.originalBranchDrifted' <<< "$leased_status")" == "false" ]] || fail 'status reported false branch drift'
|
|
576
|
+
[[ "$(jq -c '.runtime.effectiveWorktreeAllowlist' <<< "$leased_status")" == '["local/operator-note.txt","local/generated-note.txt"]' ]] || \
|
|
577
|
+
fail 'status did not expose the approved in-place allowlist snapshot'
|
|
503
578
|
if run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_acquire_repository_lease TASK-TEST-999 "$PWD" inPlaceExclusive' >/dev/null 2>&1; then
|
|
504
579
|
fail 'a second task acquired the persistent repository lease'
|
|
505
580
|
fi
|
|
@@ -508,6 +583,10 @@ pass 'persistent repository lease rejects a concurrent automation task'
|
|
|
508
583
|
[[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-004.json")" == "PENDING" ]] || fail 'prepared task did not become PENDING'
|
|
509
584
|
pass 'contract approval switches to a leased task branch while deferring the sealed plan and contract to the product commit'
|
|
510
585
|
|
|
586
|
+
git -C "$fixture" add -- \
|
|
587
|
+
.automation-worktree-allowlist \
|
|
588
|
+
local/operator-note.txt \
|
|
589
|
+
local/generated-note.txt
|
|
511
590
|
run_task "$task_root" ./scripts/automation/claim-task.sh TASK-TEST-004 >/dev/null
|
|
512
591
|
printf '%s\n' 'class OrchestratedFlowTest { fun expectedBehavior() = Unit }' > "$task_root/mobile-client/src/test/java/dev/example/orchestratorfixture/OrchestratedFlowTest.kt"
|
|
513
592
|
run_task "$task_root" ./scripts/automation/record-red.sh TASK-TEST-004 'expected missing behavior' -- dev.example.orchestratorfixture.OrchestratedFlowTest >/dev/null
|
|
@@ -517,6 +596,15 @@ run_task "$task_root" ./scripts/automation/begin-review.sh TASK-TEST-004 >/dev/n
|
|
|
517
596
|
run_task "$task_root" env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/submit-review.sh TASK-TEST-004 APPROVED 'Fresh review confirms the sealed behavior and scope.' >/dev/null
|
|
518
597
|
run_task "$task_root" ./scripts/automation/acceptance-report.sh TASK-TEST-004 >/dev/null
|
|
519
598
|
[[ "$(jq -r '.changedPaths | length' "$runtime_root/evidence/TASK-TEST-004/acceptance-report.json")" -eq 2 ]] || fail 'acceptance package omitted product paths'
|
|
599
|
+
if jq -e '.changedPaths | any(. == "local/operator-note.txt" or . == "local/generated-note.txt")' \
|
|
600
|
+
"$runtime_root/evidence/TASK-TEST-004/acceptance-report.json" >/dev/null; then
|
|
601
|
+
fail 'acceptance package included an allowlisted local path'
|
|
602
|
+
fi
|
|
603
|
+
if rg -F 'diff --git a/local/operator-note.txt b/local/operator-note.txt' "$runtime_root/evidence/TASK-TEST-004/sealed.diff" >/dev/null || \
|
|
604
|
+
rg -F 'diff --git a/local/generated-note.txt b/local/generated-note.txt' "$runtime_root/evidence/TASK-TEST-004/sealed.diff" >/dev/null || \
|
|
605
|
+
rg -F 'diff --git a/.automation-worktree-allowlist b/.automation-worktree-allowlist' "$runtime_root/evidence/TASK-TEST-004/sealed.diff" >/dev/null; then
|
|
606
|
+
fail 'sealed diff included allowlisted local content'
|
|
607
|
+
fi
|
|
520
608
|
[[ "$(jq -r '.evidence.qualityGate' "$runtime_root/evidence/TASK-TEST-004/acceptance-report.json")" == "PASSED" ]] || fail 'acceptance package omitted quality-gate status'
|
|
521
609
|
acceptance_card="$(run_fixture ./scripts/automation/show-acceptance-review.sh TASK-TEST-004)"
|
|
522
610
|
[[ "$acceptance_card" == *"人工验收提醒"* ]] || fail 'acceptance review did not actively identify the human gate'
|
|
@@ -555,6 +643,27 @@ for combined_path in \
|
|
|
555
643
|
awk -v expected="$combined_path" '$0 == expected { found = 1 } END { exit !found }' || \
|
|
556
644
|
fail "combined task commit omitted $combined_path"
|
|
557
645
|
done
|
|
646
|
+
if git -C "$fixture" diff-tree --no-commit-id --name-only -r "$combined_commit" | \
|
|
647
|
+
rg -x 'local/(operator-note|generated-note)\.txt|\.automation-worktree-allowlist' >/dev/null; then
|
|
648
|
+
fail 'combined task commit included an allowlisted local path or its control file'
|
|
649
|
+
fi
|
|
650
|
+
[[ "$(cat "$fixture/local/operator-note.txt")" == "operator local edit" ]] || fail 'tracked allowlisted content changed during integration'
|
|
651
|
+
[[ "$(cat "$fixture/local/generated-note.txt")" == "generated local state" ]] || fail 'untracked allowlisted content changed during integration'
|
|
652
|
+
staged_allowlist_paths="$(git -C "$fixture" diff --cached --name-only HEAD -- | LC_ALL=C sort)"
|
|
653
|
+
for staged_allowlist_path in \
|
|
654
|
+
.automation-worktree-allowlist \
|
|
655
|
+
local/generated-note.txt \
|
|
656
|
+
local/operator-note.txt; do
|
|
657
|
+
printf '%s\n' "$staged_allowlist_paths" | \
|
|
658
|
+
awk -v expected="$staged_allowlist_path" '$0 == expected { found = 1 } END { exit !found }' || \
|
|
659
|
+
fail "integration consumed a staged allowlisted path: $staged_allowlist_path"
|
|
660
|
+
done
|
|
661
|
+
run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_worktree_is_clean' || fail 'allowlisted local state made the integrated source root dirty'
|
|
662
|
+
pass 'in-place integration preserves allowlisted local state without evidence or commit leakage'
|
|
663
|
+
git -C "$fixture" restore --staged -- \
|
|
664
|
+
.automation-worktree-allowlist \
|
|
665
|
+
local/operator-note.txt \
|
|
666
|
+
local/generated-note.txt
|
|
558
667
|
[[ "$(jq -r '.contractCommit' "$runtime_root/evidence/TASK-TEST-004/origin.json")" == "$combined_commit" ]] || fail 'origin evidence did not bind the contract to the combined task commit'
|
|
559
668
|
[[ "$(jq -r '.pushed' "$runtime_root/evidence/TASK-TEST-004/integration.json")" == "false" ]] || fail 'integration evidence did not forbid push'
|
|
560
669
|
[[ "$(jq -r '.method' "$runtime_root/evidence/TASK-TEST-004/integration.json")" == "inPlaceExclusive-fast-forward" ]] || fail 'integration did not use the in-place fast-forward path'
|
|
@@ -579,6 +688,10 @@ jq \
|
|
|
579
688
|
> "$fixture/automation/tasks/TASK-TEST-008.json"
|
|
580
689
|
run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-008 '批准方案,生成计划和任务合同。' >/dev/null
|
|
581
690
|
run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/approve-and-run.sh TASK-TEST-008 '合同已复核,批准自动执行到人工验收阶段。' >/dev/null
|
|
691
|
+
git -C "$fixture" add -- \
|
|
692
|
+
.automation-worktree-allowlist \
|
|
693
|
+
local/operator-note.txt \
|
|
694
|
+
local/generated-note.txt
|
|
582
695
|
printf '%s\n' 'class AbortArchive { fun value() = "preserved" }' > "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/AbortArchive.kt"
|
|
583
696
|
printf '%s\n' 'must not be archived automatically' > "$fixture/out-of-contract.txt"
|
|
584
697
|
if run_fixture ./scripts/automation/abort-task.sh TASK-TEST-008 '中止任务,封存修改并恢复原分支。' >/dev/null 2>&1; then
|
|
@@ -592,10 +705,27 @@ abort_recovery_commit="$(jq -er '.recoveryCommit' "$runtime_root/evidence/TASK-T
|
|
|
592
705
|
git -C "$fixture" cat-file -e "$abort_recovery_commit:mobile-client/src/main/java/dev/example/orchestratorfixture/AbortArchive.kt" || fail 'abort recovery commit omitted the allowed change'
|
|
593
706
|
git -C "$fixture" cat-file -e "$abort_recovery_commit:docs/plans/TASK-TEST-008.md" || fail 'abort recovery commit omitted the sealed plan'
|
|
594
707
|
git -C "$fixture" cat-file -e "$abort_recovery_commit:automation/tasks/TASK-TEST-008.json" || fail 'abort recovery commit omitted the sealed contract'
|
|
708
|
+
if git -C "$fixture" diff-tree --no-commit-id --name-only -r "$abort_recovery_commit" | \
|
|
709
|
+
rg -x 'local/(operator-note|generated-note)\.txt|\.automation-worktree-allowlist' >/dev/null; then
|
|
710
|
+
fail 'abort recovery commit included an allowlisted local path or its control file'
|
|
711
|
+
fi
|
|
712
|
+
staged_abort_allowlist_paths="$(git -C "$fixture" diff --cached --name-only HEAD -- | LC_ALL=C sort)"
|
|
713
|
+
for staged_allowlist_path in \
|
|
714
|
+
.automation-worktree-allowlist \
|
|
715
|
+
local/generated-note.txt \
|
|
716
|
+
local/operator-note.txt; do
|
|
717
|
+
printf '%s\n' "$staged_abort_allowlist_paths" | \
|
|
718
|
+
awk -v expected="$staged_allowlist_path" '$0 == expected { found = 1 } END { exit !found }' || \
|
|
719
|
+
fail "abort consumed a staged allowlisted path: $staged_allowlist_path"
|
|
720
|
+
done
|
|
595
721
|
[[ ! -f "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/AbortArchive.kt" ]] || fail 'abort left archived code on the original branch'
|
|
596
722
|
[[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-008.json")" == "ABORTED" ]] || fail 'allowed abort did not reach ABORTED'
|
|
597
723
|
[[ ! -d "$runtime_root/locks/repository.workspace.lease" ]] || fail 'allowed abort did not release the repository lease'
|
|
598
724
|
pass 'abort refuses unrelated files and archives allowed uncommitted changes in a recovery commit'
|
|
725
|
+
git -C "$fixture" restore --staged -- \
|
|
726
|
+
.automation-worktree-allowlist \
|
|
727
|
+
local/operator-note.txt \
|
|
728
|
+
local/generated-note.txt
|
|
599
729
|
|
|
600
730
|
planning_only_baseline="$(git -C "$fixture" rev-parse HEAD)"
|
|
601
731
|
printf '%s\n' '# Planning-only abort plan' > "$fixture/docs/plans/TASK-TEST-009.md"
|
|
@@ -635,9 +765,17 @@ run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-007 '批
|
|
|
635
765
|
run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/approve-and-run.sh TASK-TEST-007 '合同已复核,批准自动执行到人工验收阶段。' >/dev/null
|
|
636
766
|
isolated_task_root="$(jq -er '.taskRoot' "$runtime_root/workspaces/TASK-TEST-007.json")"
|
|
637
767
|
[[ "$isolated_task_root" != "$fixture" && -d "$isolated_task_root" ]] || fail 'optional isolated strategy did not create a separate task root'
|
|
768
|
+
[[ "$(jq -r '.worktreeAllowlist | length' "$runtime_root/workspaces/TASK-TEST-007.json")" -eq 2 ]] || fail 'isolated workspace did not retain the approved allowlist snapshot'
|
|
638
769
|
[[ -f "$isolated_task_root/docs/plans/TASK-TEST-007.md" && -f "$isolated_task_root/automation/tasks/TASK-TEST-007.json" ]] || fail 'isolated task root did not receive the sealed planning artifacts'
|
|
639
770
|
[[ ! -e "$fixture/docs/plans/TASK-TEST-007.md" && ! -e "$fixture/automation/tasks/TASK-TEST-007.json" ]] || fail 'isolated preparation left duplicate planning artifacts in the source root'
|
|
640
771
|
[[ "$(git -C "$fixture" worktree list --porcelain | awk '/^worktree / { count++ } END { print count + 0 }')" -eq 2 ]] || fail 'isolated strategy created more than one additional worktree'
|
|
772
|
+
isolated_status="$(run_task "$isolated_task_root" ./scripts/automation/status.sh TASK-TEST-007)"
|
|
773
|
+
[[ "$(jq -c '.runtime.effectiveWorktreeAllowlist' <<< "$isolated_status")" == '[]' ]] || \
|
|
774
|
+
fail 'status applied the source allowlist inside an isolated task root'
|
|
775
|
+
printf '%s\n' 'task-local edit must stay visible' > "$isolated_task_root/local/operator-note.txt"
|
|
776
|
+
isolated_visible_paths="$(run_task "$isolated_task_root" bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths')"
|
|
777
|
+
printf '%s\n' "$isolated_visible_paths" | rg -x 'local/operator-note.txt' >/dev/null || fail 'source allowlist hid a task-local isolated-worktree edit'
|
|
778
|
+
git -C "$isolated_task_root" restore -- local/operator-note.txt
|
|
641
779
|
run_task "$isolated_task_root" ./scripts/automation/claim-task.sh TASK-TEST-007 >/dev/null
|
|
642
780
|
printf '%s\n' 'class IsolatedFlowTest { fun expectedBehavior() = Unit }' > "$isolated_task_root/mobile-client/src/test/java/dev/example/orchestratorfixture/IsolatedFlowTest.kt"
|
|
643
781
|
run_task "$isolated_task_root" ./scripts/automation/record-red.sh TASK-TEST-007 'expected missing behavior' -- dev.example.orchestratorfixture.IsolatedFlowTest >/dev/null
|