@frankzhang2026/opencode-android-orchestrator 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +55 -12
  3. package/dist/config/commit-message-prefix.d.ts +31 -0
  4. package/dist/config/commit-message-prefix.d.ts.map +1 -0
  5. package/dist/config/commit-message-prefix.js +155 -0
  6. package/dist/config/commit-message-prefix.js.map +1 -0
  7. package/dist/config/verification-policy.d.ts +5 -3
  8. package/dist/config/verification-policy.d.ts.map +1 -1
  9. package/dist/config/verification-policy.js +32 -13
  10. package/dist/config/verification-policy.js.map +1 -1
  11. package/dist/doctor/installation.d.ts.map +1 -1
  12. package/dist/doctor/installation.js +78 -0
  13. package/dist/doctor/installation.js.map +1 -1
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/installer/adaptive-templates.d.ts +6 -2
  19. package/dist/installer/adaptive-templates.d.ts.map +1 -1
  20. package/dist/installer/adaptive-templates.js +8 -0
  21. package/dist/installer/adaptive-templates.js.map +1 -1
  22. package/dist/installer/index.d.ts +1 -0
  23. package/dist/installer/index.d.ts.map +1 -1
  24. package/dist/installer/index.js +1 -0
  25. package/dist/installer/index.js.map +1 -1
  26. package/dist/installer/init.d.ts +4 -1
  27. package/dist/installer/init.d.ts.map +1 -1
  28. package/dist/installer/init.js +60 -9
  29. package/dist/installer/init.js.map +1 -1
  30. package/dist/installer/opencode-config.d.ts +3 -3
  31. package/dist/installer/opencode-config.d.ts.map +1 -1
  32. package/dist/installer/opencode-config.js +1 -1
  33. package/dist/installer/opencode-config.js.map +1 -1
  34. package/dist/installer/upgrade.d.ts +5 -1
  35. package/dist/installer/upgrade.d.ts.map +1 -1
  36. package/dist/installer/upgrade.js +81 -10
  37. package/dist/installer/upgrade.js.map +1 -1
  38. package/docs/MIGRATION.md +27 -14
  39. package/docs/SECURITY.md +14 -5
  40. package/docs/TROUBLESHOOTING.md +13 -11
  41. package/package.json +1 -1
  42. package/templates/README.md +9 -1
  43. package/templates/automation/config.json +2 -1
  44. package/templates/automation/config.schema.json +7 -2
  45. package/templates/scripts/automation/abort-task.sh +2 -1
  46. package/templates/scripts/automation/accept-and-integrate.sh +4 -2
  47. package/templates/scripts/automation/approve-and-run.sh +1 -0
  48. package/templates/scripts/automation/lib.sh +93 -2
  49. package/templates/scripts/automation/orchestrate-task.sh +1 -0
  50. package/templates/scripts/automation/preflight.sh +11 -0
  51. package/templates/scripts/automation/show-acceptance-review.sh +3 -0
  52. package/templates/scripts/automation/tests/run-tests.sh +59 -4
@@ -33,6 +33,7 @@ plan="$source_root/$plan_rel"
33
33
  [[ "$(git -C "$source_root" rev-parse HEAD)" == "$original_head" ]] || automation_die "original HEAD changed after contract review began"
34
34
  [[ "$(automation_file_sha256 "$contract")" == "$(jq -er '.contractSha256' "$origin_file")" ]] || automation_die "contract changed after proposal approval"
35
35
  [[ "$(automation_file_sha256 "$plan")" == "$(jq -er '.planSha256' "$origin_file")" ]] || automation_die "plan changed after proposal approval"
36
+ automation_read_commit_message_prefix_at "$source_root" >/dev/null
36
37
 
37
38
  changed_paths=()
38
39
  while IFS= read -r path; do
@@ -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,7 @@ automation_validate_config() {
111
114
  length > 0 and
112
115
  length == (unique | length) and
113
116
  all(.[]; gradle_task);
114
- .schemaVersion == 4 and
117
+ .schemaVersion == 5 and
115
118
  (.enabled | type == "boolean") and
116
119
  (.mode == "shadow" or .mode == "orchestrated") and
117
120
  (.workspaceStrategy == "inPlaceExclusive" or .workspaceStrategy == "isolatedWorktree") and
@@ -122,6 +125,7 @@ automation_validate_config() {
122
125
  (.maxReviewerRestarts | type == "number" and . >= 0 and . <= 3 and floor == .) and
123
126
  (.unitTestsEnabled | type == "boolean") and
124
127
  (.lintEnabled | type == "boolean") and
128
+ (.commitMessagePrefixMode == "required" or .commitMessagePrefixMode == "disabled") and
125
129
  (.longCommandTimeoutMs | type == "number" and . >= 120000 and . <= 7200000 and floor == .) and
126
130
  (.autoCleanupWorktrees | type == "boolean") and
127
131
  .pushAfterAcceptance == false and
@@ -246,6 +250,92 @@ automation_config_value() {
246
250
  jq -r "$query" "$AUTOMATION_CONFIG"
247
251
  }
248
252
 
253
+ automation_read_commit_message_prefix_at() {
254
+ local root="$1"
255
+ local config_file="$root/automation/config.json"
256
+ local prefix_file="$root/$AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH"
257
+ local mode byte_count line prefix prefix_bytes
258
+ local active_count=0
259
+
260
+ if [[ -L "$config_file" || ! -f "$config_file" ]]; then
261
+ automation_die "missing or unsafe automation configuration: $config_file"
262
+ return 1
263
+ fi
264
+ mode="$(jq -er '.commitMessagePrefixMode' "$config_file")" || {
265
+ automation_die "commitMessagePrefixMode is missing from $config_file"
266
+ return 1
267
+ }
268
+ case "$mode" in
269
+ disabled) return 0 ;;
270
+ required) ;;
271
+ *)
272
+ automation_die "unsupported commitMessagePrefixMode: $mode"
273
+ return 1
274
+ ;;
275
+ esac
276
+
277
+ if [[ ! -e "$prefix_file" && ! -L "$prefix_file" ]]; then
278
+ automation_die "commit-message prefix is required; fill $prefix_file before starting a task"
279
+ return 1
280
+ fi
281
+ if [[ -L "$prefix_file" || ! -f "$prefix_file" ]]; then
282
+ automation_die "commit-message prefix must be a regular file, not a symlink: $prefix_file"
283
+ return 1
284
+ fi
285
+ byte_count="$(wc -c < "$prefix_file" | tr -d '[:space:]')"
286
+ if [[ ! "$byte_count" =~ ^[0-9]+$ ]] || \
287
+ [[ "$byte_count" -gt "$AUTOMATION_COMMIT_MESSAGE_PREFIX_FILE_MAX_BYTES" ]]; then
288
+ automation_die "commit-message prefix file exceeds $AUTOMATION_COMMIT_MESSAGE_PREFIX_FILE_MAX_BYTES bytes: $prefix_file"
289
+ return 1
290
+ fi
291
+
292
+ while IFS= read -r line || [[ -n "$line" ]]; do
293
+ line="${line%$'\r'}"
294
+ [[ -z "$line" || "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]] && continue
295
+ active_count=$((active_count + 1))
296
+ if [[ "$active_count" -gt 1 ]]; then
297
+ automation_die "commit-message prefix must contain exactly one non-comment line: $prefix_file"
298
+ return 1
299
+ fi
300
+ if [[ "$line" == [[:space:]]* || "$line" == *[[:space:]] ]]; then
301
+ automation_die "commit-message prefix must not have leading or trailing whitespace"
302
+ return 1
303
+ fi
304
+ case "$line" in
305
+ *[[:cntrl:]]*)
306
+ automation_die "commit-message prefix must not contain control characters"
307
+ return 1
308
+ ;;
309
+ esac
310
+ prefix="$line"
311
+ done < "$prefix_file"
312
+
313
+ if [[ "$active_count" -eq 0 ]]; then
314
+ automation_die "commit-message prefix is not configured; fill $prefix_file before starting a task"
315
+ return 1
316
+ fi
317
+ prefix_bytes="$(printf '%s' "$prefix" | wc -c | tr -d '[:space:]')"
318
+ if [[ ! "$prefix_bytes" =~ ^[0-9]+$ ]] || \
319
+ [[ "$prefix_bytes" -gt "$AUTOMATION_COMMIT_MESSAGE_PREFIX_MAX_BYTES" ]]; then
320
+ automation_die "commit-message prefix exceeds $AUTOMATION_COMMIT_MESSAGE_PREFIX_MAX_BYTES UTF-8 bytes"
321
+ return 1
322
+ fi
323
+ printf '%s\n' "$prefix"
324
+ }
325
+
326
+ automation_commit_message_at() {
327
+ local root="$1"
328
+ local base_message="$2"
329
+ local prefix
330
+
331
+ prefix="$(automation_read_commit_message_prefix_at "$root")" || return 1
332
+ if [[ -n "$prefix" ]]; then
333
+ printf '%s %s\n' "$prefix" "$base_message"
334
+ else
335
+ printf '%s\n' "$base_message"
336
+ fi
337
+ }
338
+
249
339
  automation_validate_gradle_task() {
250
340
  local task="${1:-}"
251
341
  local safe_task_pattern='^([A-Za-z][A-Za-z0-9_.-]*|(:[A-Za-z0-9_.-]+)+)$'
@@ -453,7 +543,7 @@ automation_validate_worktree_allowlist_entry_at() {
453
543
  ;;
454
544
  esac
455
545
  case "$path" in
456
- /*|./*|.|..|.git|.git/*|docs/plans|docs/plans/*|"$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH")
546
+ /*|./*|.|..|.git|.git/*|docs/plans|docs/plans/*|"$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH"|"$AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH")
457
547
  automation_die "worktree allowlist entry is reserved or unsafe: $path"
458
548
  return 1
459
549
  ;;
@@ -608,6 +698,7 @@ automation_worktree_path_is_allowlisted() {
608
698
  local entry
609
699
 
610
700
  [[ "$path" == "$AUTOMATION_WORKTREE_ALLOWLIST_RELATIVE_PATH" ]] && return 0
701
+ [[ "$path" == "$AUTOMATION_COMMIT_MESSAGE_PREFIX_RELATIVE_PATH" ]] && return 0
611
702
  while IFS= read -r entry; do
612
703
  [[ -n "$entry" ]] || continue
613
704
  [[ "$path" == "$entry" ]] && return 0
@@ -19,6 +19,7 @@ source_root="$(jq -er '.sourceRoot' "$workspace_file")"
19
19
  if [[ "$(jq -r '.repositoryLeaseRequired // false' "$workspace_file")" == "true" ]]; then
20
20
  automation_require_repository_lease "$task_id" "$source_root" "$workspace_strategy"
21
21
  fi
22
+ automation_read_commit_message_prefix_at "$source_root" >/dev/null
22
23
 
23
24
  automation_acquire_run_lock "$task_id"
24
25
  trap 'automation_release_run_lock' EXIT
@@ -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"
@@ -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'
@@ -110,7 +110,7 @@ printf '%s\n' \
110
110
  chmod +x "$fixture/gradlew"
111
111
 
112
112
  jq -n '{
113
- schemaVersion: 4,
113
+ schemaVersion: 5,
114
114
  enabled: true,
115
115
  mode: "orchestrated",
116
116
  workspaceStrategy: "inPlaceExclusive",
@@ -121,6 +121,7 @@ jq -n '{
121
121
  maxReviewerRestarts: 2,
122
122
  unitTestsEnabled: true,
123
123
  lintEnabled: false,
124
+ commitMessagePrefixMode: "required",
124
125
  longCommandTimeoutMs: 1800000,
125
126
  autoCleanupWorktrees: true,
126
127
  pushAfterAcceptance: false,
@@ -249,6 +250,8 @@ jq -n '{
249
250
  git commit -qm 'Create automation fixture'
250
251
  )
251
252
 
253
+ printf '%s\n' '批次甲' > "$fixture/automation/automation-commit-prefix"
254
+
252
255
  printf '%s\n' \
253
256
  '# Exact repository-relative paths intentionally kept local' \
254
257
  'local/operator-note.txt' \
@@ -260,6 +263,37 @@ run_fixture ./scripts/automation/preflight.sh --source >/dev/null
260
263
  allowlisted_changes="$(run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths')"
261
264
  [[ -z "$allowlisted_changes" ]] || fail "allowlisted paths remained visible: $allowlisted_changes"
262
265
 
266
+ printf '%s\n' \
267
+ '# 必填:请在下一行填写当前 Git 提交文案前缀' \
268
+ '# 示例:XXX' \
269
+ > "$fixture/automation/automation-commit-prefix"
270
+ if run_fixture ./scripts/automation/preflight.sh --source >/dev/null 2>&1; then
271
+ fail 'normal preflight accepted an unconfigured required commit prefix'
272
+ fi
273
+ run_fixture ./scripts/automation/preflight.sh --shadow >/dev/null 2>&1 || \
274
+ fail 'shadow preflight rejected the intentionally unconfigured installation template'
275
+ rm "$fixture/automation/automation-commit-prefix"
276
+ if run_fixture ./scripts/automation/preflight.sh --source >/dev/null 2>&1; then
277
+ fail 'normal preflight accepted a missing required commit prefix'
278
+ fi
279
+ run_fixture ./scripts/automation/preflight.sh --shadow >/dev/null 2>&1 || \
280
+ fail 'shadow preflight rejected a missing required commit prefix'
281
+ printf '%s\n' '批次甲' > "$fixture/automation/automation-commit-prefix"
282
+ pass 'required missing or unconfigured commit prefix blocks task startup but not shadow verification'
283
+
284
+ printf '%s\n' '批次甲' '第二行无效' > "$fixture/automation/automation-commit-prefix"
285
+ if run_fixture ./scripts/automation/preflight.sh --source >/dev/null 2>&1; then
286
+ fail 'preflight accepted more than one active commit-prefix line'
287
+ fi
288
+ rm "$fixture/automation/automation-commit-prefix"
289
+ ln -s ../local/operator-note.txt "$fixture/automation/automation-commit-prefix"
290
+ if run_fixture ./scripts/automation/preflight.sh --source >/dev/null 2>&1; then
291
+ fail 'preflight accepted a symbolic-link commit-prefix file'
292
+ fi
293
+ rm "$fixture/automation/automation-commit-prefix"
294
+ printf '%s\n' '批次甲' > "$fixture/automation/automation-commit-prefix"
295
+ pass 'invalid and symbolic-link commit-prefix files fail closed'
296
+
263
297
  printf '%s\n' \
264
298
  'local/operator-note.txt' \
265
299
  'local/generated-note.txt' \
@@ -288,6 +322,10 @@ printf '%s\n' 'automation/config.json' > "$fixture/.automation-worktree-allowlis
288
322
  if run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths >/dev/null' 2>/dev/null; then
289
323
  fail 'worktree allowlist accepted a protected automation path'
290
324
  fi
325
+ printf '%s\n' 'automation/automation-commit-prefix' > "$fixture/.automation-worktree-allowlist"
326
+ if run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths >/dev/null' 2>/dev/null; then
327
+ fail 'worktree allowlist accepted the reserved commit-prefix control file'
328
+ fi
291
329
  printf '%s\n' 'local/*.json' > "$fixture/.automation-worktree-allowlist"
292
330
  if run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_changed_paths >/dev/null' 2>/dev/null; then
293
331
  fail 'worktree allowlist accepted a glob pattern'
@@ -595,6 +633,11 @@ approved_baseline="$(jq -er '.baselineHead' "$runtime_root/workspaces/TASK-TEST-
595
633
  [[ "$(git -C "$fixture" rev-list --count "$approved_baseline..automation/task-test-004")" -eq 0 ]] || fail 'contract approval created a planning-only commit'
596
634
  [[ "$(jq -r '.contractCommit' "$runtime_root/evidence/TASK-TEST-004/origin.json")" == "null" ]] || fail 'contract approval recorded a commit before product integration'
597
635
  [[ "$(git -C "$fixture" status --porcelain --untracked-files=all -- docs/plans/TASK-TEST-004.md automation/tasks/TASK-TEST-004.json | wc -l | tr -d ' ')" -eq 2 ]] || fail 'sealed planning artifacts were not left pending on the task branch'
636
+ printf '%s\n' '# 尚未填写提交前缀' > "$fixture/automation/automation-commit-prefix"
637
+ if run_task "$task_root" env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/orchestrate-task.sh TASK-TEST-004 >/dev/null 2>&1; then
638
+ fail 'direct task launcher bypassed the required commit-prefix gate'
639
+ fi
640
+ printf '%s\n' '批次甲' > "$fixture/automation/automation-commit-prefix"
598
641
  [[ -f "$runtime_root/locks/repository.workspace.lease/lease.json" ]] || fail 'persistent repository lease was not created'
599
642
  leased_status="$(run_fixture ./scripts/automation/status.sh TASK-TEST-004)"
600
643
  [[ "$(jq -r '.runtime.repositoryLeaseMatches' <<< "$leased_status")" == "true" ]] || fail 'status did not verify the repository lease'
@@ -611,6 +654,7 @@ pass 'contract approval switches to a leased task branch while deferring the sea
611
654
 
612
655
  git -C "$fixture" add -- \
613
656
  .automation-worktree-allowlist \
657
+ automation/automation-commit-prefix \
614
658
  local/operator-note.txt \
615
659
  local/generated-note.txt
616
660
  run_task "$task_root" ./scripts/automation/claim-task.sh TASK-TEST-004 >/dev/null
@@ -628,7 +672,8 @@ if jq -e '.changedPaths | any(. == "local/operator-note.txt" or . == "local/gene
628
672
  fi
629
673
  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 || \
630
674
  rg -F 'diff --git a/local/generated-note.txt b/local/generated-note.txt' "$runtime_root/evidence/TASK-TEST-004/sealed.diff" >/dev/null || \
631
- rg -F 'diff --git a/.automation-worktree-allowlist b/.automation-worktree-allowlist' "$runtime_root/evidence/TASK-TEST-004/sealed.diff" >/dev/null; then
675
+ rg -F 'diff --git a/.automation-worktree-allowlist b/.automation-worktree-allowlist' "$runtime_root/evidence/TASK-TEST-004/sealed.diff" >/dev/null || \
676
+ rg -F 'diff --git a/automation/automation-commit-prefix b/automation/automation-commit-prefix' "$runtime_root/evidence/TASK-TEST-004/sealed.diff" >/dev/null; then
632
677
  fail 'sealed diff included allowlisted local content'
633
678
  fi
634
679
  [[ "$(jq -r '.evidence.qualityGate' "$runtime_root/evidence/TASK-TEST-004/acceptance-report.json")" == "PASSED" ]] || fail 'acceptance package omitted quality-gate status'
@@ -637,6 +682,7 @@ acceptance_card="$(run_fixture ./scripts/automation/show-acceptance-review.sh TA
637
682
  [[ "$acceptance_card" == *"P0 · 真实行为是否满足合同"* ]] || fail 'acceptance review omitted behavioral focus'
638
683
  [[ "$acceptance_card" == *"P0 · 旧行为与范围是否被误伤"* ]] || fail 'acceptance review omitted regression and scope focus'
639
684
  [[ "$acceptance_card" == *"sealed diff SHA"* ]] || fail 'acceptance review omitted sealed binding'
685
+ [[ "$acceptance_card" == *"批次甲 Implement"* ]] || fail 'acceptance review omitted the current prefixed commit message'
640
686
  [[ "$acceptance_card" == *"成功集成后自动删除;失败或阻塞时保留"* ]] || fail 'acceptance review omitted task branch cleanup policy'
641
687
  pass 'automated evidence becomes one focused, SHA-verified human acceptance card'
642
688
 
@@ -654,8 +700,10 @@ if run_fixture env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/accept-and-integ
654
700
  fi
655
701
  pass 'integrator requires final acceptance bound to the sealed diff'
656
702
 
703
+ printf '%s\n' '批次甲-提交时' > "$fixture/automation/automation-commit-prefix"
657
704
  run_fixture env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/accept-and-integrate.sh TASK-TEST-004 '验收通过,提交到原分支。' >/dev/null
658
705
  combined_commit="$(jq -er '.productCommit' "$runtime_root/workspaces/TASK-TEST-004.json")"
706
+ [[ "$(git -C "$fixture" show -s --format=%s "$combined_commit")" == "批次甲-提交时 Implement "* ]] || fail 'combined task commit did not re-read the latest prefix at commit time'
659
707
  [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-004.json")" == "COMPLETED" ]] || fail 'final integration did not reach COMPLETED'
660
708
  [[ "$(git -C "$fixture" symbolic-ref --short HEAD)" == "$original_branch" ]] || fail 'integrator changed the original branch identity'
661
709
  [[ -f "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/OrchestratedFlow.kt" ]] || fail 'product change was not integrated into original branch'
@@ -670,7 +718,7 @@ for combined_path in \
670
718
  fail "combined task commit omitted $combined_path"
671
719
  done
672
720
  if git -C "$fixture" diff-tree --no-commit-id --name-only -r "$combined_commit" | \
673
- rg -x 'local/(operator-note|generated-note)\.txt|\.automation-worktree-allowlist' >/dev/null; then
721
+ rg -x 'local/(operator-note|generated-note)\.txt|\.automation-worktree-allowlist|automation/automation-commit-prefix' >/dev/null; then
674
722
  fail 'combined task commit included an allowlisted local path or its control file'
675
723
  fi
676
724
  [[ "$(cat "$fixture/local/operator-note.txt")" == "operator local edit" ]] || fail 'tracked allowlisted content changed during integration'
@@ -678,6 +726,7 @@ fi
678
726
  staged_allowlist_paths="$(git -C "$fixture" diff --cached --name-only HEAD -- | LC_ALL=C sort)"
679
727
  for staged_allowlist_path in \
680
728
  .automation-worktree-allowlist \
729
+ automation/automation-commit-prefix \
681
730
  local/generated-note.txt \
682
731
  local/operator-note.txt; do
683
732
  printf '%s\n' "$staged_allowlist_paths" | \
@@ -688,6 +737,7 @@ run_fixture bash -c 'source ./scripts/automation/lib.sh; automation_worktree_is_
688
737
  pass 'in-place integration preserves allowlisted local state without evidence or commit leakage'
689
738
  git -C "$fixture" restore --staged -- \
690
739
  .automation-worktree-allowlist \
740
+ automation/automation-commit-prefix \
691
741
  local/operator-note.txt \
692
742
  local/generated-note.txt
693
743
  [[ "$(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'
@@ -714,8 +764,10 @@ jq \
714
764
  > "$fixture/automation/tasks/TASK-TEST-008.json"
715
765
  run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-008 '批准方案,生成计划和任务合同。' >/dev/null
716
766
  run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/approve-and-run.sh TASK-TEST-008 '合同已复核,批准自动执行到人工验收阶段。' >/dev/null
767
+ printf '%s\n' '批次乙' > "$fixture/automation/automation-commit-prefix"
717
768
  git -C "$fixture" add -- \
718
769
  .automation-worktree-allowlist \
770
+ automation/automation-commit-prefix \
719
771
  local/operator-note.txt \
720
772
  local/generated-note.txt
721
773
  printf '%s\n' 'class AbortArchive { fun value() = "preserved" }' > "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/AbortArchive.kt"
@@ -728,16 +780,18 @@ fi
728
780
  rm "$fixture/out-of-contract.txt"
729
781
  run_fixture ./scripts/automation/abort-task.sh TASK-TEST-008 '中止任务,封存修改并恢复原分支。' >/dev/null
730
782
  abort_recovery_commit="$(jq -er '.recoveryCommit' "$runtime_root/evidence/TASK-TEST-008/abort.json")"
783
+ [[ "$(git -C "$fixture" show -s --format=%s "$abort_recovery_commit")" == "批次乙 Archive aborted work for TASK-TEST-008" ]] || fail 'abort recovery commit did not read the updated prefix'
731
784
  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'
732
785
  git -C "$fixture" cat-file -e "$abort_recovery_commit:docs/plans/TASK-TEST-008.md" || fail 'abort recovery commit omitted the sealed plan'
733
786
  git -C "$fixture" cat-file -e "$abort_recovery_commit:automation/tasks/TASK-TEST-008.json" || fail 'abort recovery commit omitted the sealed contract'
734
787
  if git -C "$fixture" diff-tree --no-commit-id --name-only -r "$abort_recovery_commit" | \
735
- rg -x 'local/(operator-note|generated-note)\.txt|\.automation-worktree-allowlist' >/dev/null; then
788
+ rg -x 'local/(operator-note|generated-note)\.txt|\.automation-worktree-allowlist|automation/automation-commit-prefix' >/dev/null; then
736
789
  fail 'abort recovery commit included an allowlisted local path or its control file'
737
790
  fi
738
791
  staged_abort_allowlist_paths="$(git -C "$fixture" diff --cached --name-only HEAD -- | LC_ALL=C sort)"
739
792
  for staged_allowlist_path in \
740
793
  .automation-worktree-allowlist \
794
+ automation/automation-commit-prefix \
741
795
  local/generated-note.txt \
742
796
  local/operator-note.txt; do
743
797
  printf '%s\n' "$staged_abort_allowlist_paths" | \
@@ -750,6 +804,7 @@ done
750
804
  pass 'abort refuses unrelated files and archives allowed uncommitted changes in a recovery commit'
751
805
  git -C "$fixture" restore --staged -- \
752
806
  .automation-worktree-allowlist \
807
+ automation/automation-commit-prefix \
753
808
  local/operator-note.txt \
754
809
  local/generated-note.txt
755
810