@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.
Files changed (62) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +85 -43
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/dist/cli.js +127 -10
  5. package/dist/cli.js.map +1 -1
  6. package/dist/doctor/installation.d.ts.map +1 -1
  7. package/dist/doctor/installation.js +29 -4
  8. package/dist/doctor/installation.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/installer/adaptive-templates.d.ts +25 -3
  14. package/dist/installer/adaptive-templates.d.ts.map +1 -1
  15. package/dist/installer/adaptive-templates.js +75 -9
  16. package/dist/installer/adaptive-templates.js.map +1 -1
  17. package/dist/installer/index.d.ts +2 -2
  18. package/dist/installer/index.d.ts.map +1 -1
  19. package/dist/installer/index.js +2 -2
  20. package/dist/installer/index.js.map +1 -1
  21. package/dist/installer/init.d.ts +2 -1
  22. package/dist/installer/init.d.ts.map +1 -1
  23. package/dist/installer/init.js +11 -2
  24. package/dist/installer/init.js.map +1 -1
  25. package/dist/installer/opencode-config.d.ts +4 -3
  26. package/dist/installer/opencode-config.d.ts.map +1 -1
  27. package/dist/installer/opencode-config.js +10 -4
  28. package/dist/installer/opencode-config.js.map +1 -1
  29. package/dist/installer/upgrade.d.ts +5 -0
  30. package/dist/installer/upgrade.d.ts.map +1 -1
  31. package/dist/installer/upgrade.js +44 -10
  32. package/dist/installer/upgrade.js.map +1 -1
  33. package/docs/MIGRATION.md +25 -14
  34. package/docs/SECURITY.md +29 -5
  35. package/docs/TROUBLESHOOTING.md +25 -6
  36. package/package.json +1 -1
  37. package/templates/.opencode/agents/scheduled-coder.md +3 -0
  38. package/templates/.opencode/agents/scheduled-reviewer.md +5 -4
  39. package/templates/.opencode/skills/scheduled-quality-coder/SKILL.md +10 -6
  40. package/templates/.opencode/skills/scheduled-quality-reviewer/SKILL.md +7 -4
  41. package/templates/AGENTS.md.fragment +4 -0
  42. package/templates/README.md +5 -2
  43. package/templates/automation/config.json +18 -0
  44. package/templates/automation/config.schema.json +31 -0
  45. package/templates/automation/task-contract.schema.json +22 -1
  46. package/templates/automation/tasks/TASK-TEMPLATE.json.example +5 -1
  47. package/templates/scripts/automation/abort-task.sh +5 -10
  48. package/templates/scripts/automation/accept-and-integrate.sh +1 -1
  49. package/templates/scripts/automation/acceptance-report.sh +2 -9
  50. package/templates/scripts/automation/approve-and-run.sh +3 -0
  51. package/templates/scripts/automation/claim-task.sh +6 -6
  52. package/templates/scripts/automation/lib.sh +403 -8
  53. package/templates/scripts/automation/orchestrate-task.sh +1 -1
  54. package/templates/scripts/automation/preflight.sh +1 -1
  55. package/templates/scripts/automation/record-red.sh +7 -9
  56. package/templates/scripts/automation/scope-gate.sh +12 -9
  57. package/templates/scripts/automation/show-acceptance-review.sh +1 -1
  58. package/templates/scripts/automation/status.sh +4 -0
  59. package/templates/scripts/automation/tests/run-tests.sh +164 -9
  60. package/templates/scripts/automation/validate-contract.sh +16 -5
  61. package/templates/scripts/automation/verify-integration.sh +12 -11
  62. package/templates/scripts/automation/verify-task.sh +13 -28
@@ -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' \
@@ -137,10 +140,18 @@ jq -n '{
137
140
  "systematic-debugging",
138
141
  "verification-before-completion"
139
142
  ],
143
+ gradleVerification: {
144
+ fullUnitTestTasks: ["testDebugUnitTest"],
145
+ focusedTestTasks: ["testDebugUnitTest"],
146
+ assembleTasks: ["assembleDebug"],
147
+ lintTasks: ["lint"],
148
+ deviceTestTasks: ["connectedDebugAndroidTest"]
149
+ },
140
150
  androidProject: {
141
151
  name: "automation-shell-fixture",
142
152
  gradleDsl: "kotlin",
143
153
  settingsFile: "settings.gradle.kts",
154
+ moduleScope: "all",
144
155
  primaryModule: ":mobile-client",
145
156
  modules: [{
146
157
  gradlePath: ":mobile-client",
@@ -158,6 +169,7 @@ jq -n '{
158
169
  ]
159
170
  },
160
171
  protectedPaths: [
172
+ ".automation-worktree-allowlist",
161
173
  ".opencode/",
162
174
  ".automation-plugin/",
163
175
  "automation/",
@@ -192,6 +204,7 @@ jq -n '{
192
204
  "mobile-client/src/test/java/dev/example/orchestratorfixture/**"
193
205
  ],
194
206
  forbiddenPaths: [
207
+ ".automation-worktree-allowlist",
195
208
  ".opencode/**",
196
209
  ".automation-plugin/**",
197
210
  "automation/**",
@@ -217,7 +230,10 @@ jq -n '{
217
230
  ],
218
231
  acceptanceCriteria: ["Greeting returns the approved value"],
219
232
  nonGoals: ["No unrelated refactoring"],
220
- targetTests: ["dev.example.orchestratorfixture.GreetingTest"],
233
+ targetTests: [{
234
+ gradleTask: "testDebugUnitTest",
235
+ filter: "dev.example.orchestratorfixture.GreetingTest"
236
+ }],
221
237
  deviceTestsRequired: false,
222
238
  testPolicy: "required",
223
239
  testPolicyReason: "The behavior requires a focused regression test"
@@ -232,6 +248,74 @@ jq -n '{
232
248
  git commit -qm 'Create automation fixture'
233
249
  )
234
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
+
235
319
  run_fixture ./scripts/automation/validate-contract.sh TASK-TEST-001 >/dev/null
236
320
  pass 'valid version-3 configuration and task contract are accepted'
237
321
 
@@ -273,11 +357,17 @@ pass 'illegal state transition is rejected'
273
357
  run_fixture ./scripts/automation/claim-task.sh TASK-TEST-001 >/dev/null
274
358
  [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-001.json")" == "CODING" ]] || fail 'claim did not create CODING state'
275
359
  [[ -f "$runtime_root/evidence/TASK-TEST-001/baseline.json" ]] || fail 'baseline metadata missing'
360
+ jq -e \
361
+ '.command == ["./gradlew", "testDebugUnitTest"]' \
362
+ "$runtime_root/evidence/TASK-TEST-001/baseline.json" >/dev/null || fail 'baseline command does not match the configured task matrix'
276
363
  pass 'claim verifies workspace identity and records a green baseline'
277
364
 
278
365
  printf '%s\n' 'class GreetingTest { fun expectedBehavior() = Unit }' > "$fixture/mobile-client/src/test/java/dev/example/orchestratorfixture/GreetingTest.kt"
279
366
  run_fixture ./scripts/automation/record-red.sh TASK-TEST-001 'expected missing behavior' -- dev.example.orchestratorfixture.GreetingTest >/dev/null
280
367
  [[ "$(jq -r '.exitCode' "$runtime_root/evidence/TASK-TEST-001/red.json")" -ne 0 ]] || fail 'RED evidence exit code was not captured'
368
+ jq -e \
369
+ '.command == ["./gradlew", "testDebugUnitTest", "--tests", "dev.example.orchestratorfixture.GreetingTest"]' \
370
+ "$runtime_root/evidence/TASK-TEST-001/red.json" >/dev/null || fail 'RED command does not bind the configured task and filter'
281
371
  pass 'focused failing test records genuine RED evidence'
282
372
 
283
373
  printf '%s\n' 'unsafe' > "$fixture/automation/forbidden-change.txt"
@@ -338,7 +428,7 @@ jq \
338
428
  '.id = "TASK-TEST-002" |
339
429
  .title = "Exercise the bounded verification failure loop" |
340
430
  .planPath = "docs/plans/TASK-TEST-002.md" |
341
- .targetTests = ["dev.example.orchestratorfixture.FailureLoopTest"]' \
431
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.FailureLoopTest"}]' \
342
432
  "$fixture/automation/tasks/TASK-TEST-001.json" \
343
433
  > "$fixture/automation/tasks/TASK-TEST-002.json"
344
434
  (
@@ -375,7 +465,7 @@ jq \
375
465
  '.id = "TASK-TEST-003" |
376
466
  .title = "Verify dirty worktree preflight blocking" |
377
467
  .planPath = "docs/plans/TASK-TEST-003.md" |
378
- .targetTests = ["dev.example.orchestratorfixture.PreflightBlockTest"]' \
468
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.PreflightBlockTest"}]' \
379
469
  "$fixture/automation/tasks/TASK-TEST-001.json" \
380
470
  > "$fixture/automation/tasks/TASK-TEST-003.json"
381
471
  (
@@ -402,7 +492,7 @@ jq \
402
492
  '.id = "TASK-TEST-005" |
403
493
  .title = "Exercise the bounded reviewer repair cycle" |
404
494
  .planPath = "docs/plans/TASK-TEST-005.md" |
405
- .targetTests = ["dev.example.orchestratorfixture.ReviewerFixTest"]' \
495
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.ReviewerFixTest"}]' \
406
496
  "$fixture/automation/tasks/TASK-TEST-001.json" \
407
497
  > "$fixture/automation/tasks/TASK-TEST-005.json"
408
498
  (
@@ -452,7 +542,7 @@ jq \
452
542
  '.id = "TASK-TEST-004" |
453
543
  .title = "Exercise in-place transactional integration" |
454
544
  .planPath = "docs/plans/TASK-TEST-004.md" |
455
- .targetTests = ["dev.example.orchestratorfixture.OrchestratedFlowTest"]' \
545
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.OrchestratedFlowTest"}]' \
456
546
  "$fixture/automation/tasks/TASK-TEST-001.json" \
457
547
  > "$fixture/automation/tasks/TASK-TEST-004.json"
458
548
 
@@ -483,6 +573,8 @@ approved_baseline="$(jq -er '.baselineHead' "$runtime_root/workspaces/TASK-TEST-
483
573
  leased_status="$(run_fixture ./scripts/automation/status.sh TASK-TEST-004)"
484
574
  [[ "$(jq -r '.runtime.repositoryLeaseMatches' <<< "$leased_status")" == "true" ]] || fail 'status did not verify the repository lease'
485
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'
486
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
487
579
  fail 'a second task acquired the persistent repository lease'
488
580
  fi
@@ -491,6 +583,10 @@ pass 'persistent repository lease rejects a concurrent automation task'
491
583
  [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-004.json")" == "PENDING" ]] || fail 'prepared task did not become PENDING'
492
584
  pass 'contract approval switches to a leased task branch while deferring the sealed plan and contract to the product commit'
493
585
 
586
+ git -C "$fixture" add -- \
587
+ .automation-worktree-allowlist \
588
+ local/operator-note.txt \
589
+ local/generated-note.txt
494
590
  run_task "$task_root" ./scripts/automation/claim-task.sh TASK-TEST-004 >/dev/null
495
591
  printf '%s\n' 'class OrchestratedFlowTest { fun expectedBehavior() = Unit }' > "$task_root/mobile-client/src/test/java/dev/example/orchestratorfixture/OrchestratedFlowTest.kt"
496
592
  run_task "$task_root" ./scripts/automation/record-red.sh TASK-TEST-004 'expected missing behavior' -- dev.example.orchestratorfixture.OrchestratedFlowTest >/dev/null
@@ -500,6 +596,15 @@ run_task "$task_root" ./scripts/automation/begin-review.sh TASK-TEST-004 >/dev/n
500
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
501
597
  run_task "$task_root" ./scripts/automation/acceptance-report.sh TASK-TEST-004 >/dev/null
502
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
503
608
  [[ "$(jq -r '.evidence.qualityGate' "$runtime_root/evidence/TASK-TEST-004/acceptance-report.json")" == "PASSED" ]] || fail 'acceptance package omitted quality-gate status'
504
609
  acceptance_card="$(run_fixture ./scripts/automation/show-acceptance-review.sh TASK-TEST-004)"
505
610
  [[ "$acceptance_card" == *"人工验收提醒"* ]] || fail 'acceptance review did not actively identify the human gate'
@@ -538,6 +643,27 @@ for combined_path in \
538
643
  awk -v expected="$combined_path" '$0 == expected { found = 1 } END { exit !found }' || \
539
644
  fail "combined task commit omitted $combined_path"
540
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
541
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'
542
668
  [[ "$(jq -r '.pushed' "$runtime_root/evidence/TASK-TEST-004/integration.json")" == "false" ]] || fail 'integration evidence did not forbid push'
543
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'
@@ -557,11 +683,15 @@ jq \
557
683
  '.id = "TASK-TEST-008" |
558
684
  .title = "Archive an interrupted in-place task" |
559
685
  .planPath = "docs/plans/TASK-TEST-008.md" |
560
- .targetTests = ["dev.example.orchestratorfixture.AbortArchiveTest"]' \
686
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.AbortArchiveTest"}]' \
561
687
  "$fixture/automation/tasks/TASK-TEST-001.json" \
562
688
  > "$fixture/automation/tasks/TASK-TEST-008.json"
563
689
  run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-008 '批准方案,生成计划和任务合同。' >/dev/null
564
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
565
695
  printf '%s\n' 'class AbortArchive { fun value() = "preserved" }' > "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/AbortArchive.kt"
566
696
  printf '%s\n' 'must not be archived automatically' > "$fixture/out-of-contract.txt"
567
697
  if run_fixture ./scripts/automation/abort-task.sh TASK-TEST-008 '中止任务,封存修改并恢复原分支。' >/dev/null 2>&1; then
@@ -575,10 +705,27 @@ abort_recovery_commit="$(jq -er '.recoveryCommit' "$runtime_root/evidence/TASK-T
575
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'
576
706
  git -C "$fixture" cat-file -e "$abort_recovery_commit:docs/plans/TASK-TEST-008.md" || fail 'abort recovery commit omitted the sealed plan'
577
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
578
721
  [[ ! -f "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/AbortArchive.kt" ]] || fail 'abort left archived code on the original branch'
579
722
  [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-008.json")" == "ABORTED" ]] || fail 'allowed abort did not reach ABORTED'
580
723
  [[ ! -d "$runtime_root/locks/repository.workspace.lease" ]] || fail 'allowed abort did not release the repository lease'
581
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
582
729
 
583
730
  planning_only_baseline="$(git -C "$fixture" rev-parse HEAD)"
584
731
  printf '%s\n' '# Planning-only abort plan' > "$fixture/docs/plans/TASK-TEST-009.md"
@@ -586,7 +733,7 @@ jq \
586
733
  '.id = "TASK-TEST-009" |
587
734
  .title = "Abort before product editing" |
588
735
  .planPath = "docs/plans/TASK-TEST-009.md" |
589
- .targetTests = ["dev.example.orchestratorfixture.PlanningOnlyAbortTest"]' \
736
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.PlanningOnlyAbortTest"}]' \
590
737
  "$fixture/automation/tasks/TASK-TEST-001.json" \
591
738
  > "$fixture/automation/tasks/TASK-TEST-009.json"
592
739
  run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-009 '批准方案,生成计划和任务合同。' >/dev/null
@@ -611,16 +758,24 @@ jq \
611
758
  '.id = "TASK-TEST-007" |
612
759
  .title = "Keep optional isolated workspace support" |
613
760
  .planPath = "docs/plans/TASK-TEST-007.md" |
614
- .targetTests = ["dev.example.orchestratorfixture.IsolatedFlowTest"]' \
761
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.IsolatedFlowTest"}]' \
615
762
  "$fixture/automation/tasks/TASK-TEST-001.json" \
616
763
  > "$fixture/automation/tasks/TASK-TEST-007.json"
617
764
  run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-007 '批准方案,生成计划和任务合同。' >/dev/null
618
765
  run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/approve-and-run.sh TASK-TEST-007 '合同已复核,批准自动执行到人工验收阶段。' >/dev/null
619
766
  isolated_task_root="$(jq -er '.taskRoot' "$runtime_root/workspaces/TASK-TEST-007.json")"
620
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'
621
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'
622
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'
623
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
624
779
  run_task "$isolated_task_root" ./scripts/automation/claim-task.sh TASK-TEST-007 >/dev/null
625
780
  printf '%s\n' 'class IsolatedFlowTest { fun expectedBehavior() = Unit }' > "$isolated_task_root/mobile-client/src/test/java/dev/example/orchestratorfixture/IsolatedFlowTest.kt"
626
781
  run_task "$isolated_task_root" ./scripts/automation/record-red.sh TASK-TEST-007 'expected missing behavior' -- dev.example.orchestratorfixture.IsolatedFlowTest >/dev/null
@@ -654,7 +809,7 @@ jq \
654
809
  '.id = "TASK-TEST-006" |
655
810
  .title = "Block an advanced original branch" |
656
811
  .planPath = "docs/plans/TASK-TEST-006.md" |
657
- .targetTests = ["dev.example.orchestratorfixture.AdvancedOriginalTest"]' \
812
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.AdvancedOriginalTest"}]' \
658
813
  "$fixture/automation/tasks/TASK-TEST-001.json" \
659
814
  > "$fixture/automation/tasks/TASK-TEST-006.json"
660
815
  run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-006 '批准方案,生成计划和任务合同。' >/dev/null
@@ -13,6 +13,7 @@ input="${1:-}"
13
13
  [[ -n "$input" ]] || { usage; exit 2; }
14
14
  automation_require_command jq
15
15
  automation_require_layout
16
+ automation_validate_config
16
17
 
17
18
  if [[ "$input" == */* || "$input" == *.json || "$input" == *.example ]]; then
18
19
  contract="$input"
@@ -39,7 +40,12 @@ jq -e '
39
40
  (.allowedSuperpowers == ["test-driven-development", "systematic-debugging", "verification-before-completion"]) and
40
41
  (.acceptanceCriteria | type == "array" and length > 0) and
41
42
  (.nonGoals | type == "array" and length > 0) and
42
- (.targetTests | type == "array" and length > 0) and
43
+ (.targetTests | type == "array" and length > 0 and all(.[];
44
+ type == "object" and
45
+ keys == ["filter", "gradleTask"] and
46
+ (.gradleTask | type == "string" and test("^(?:[A-Za-z][A-Za-z0-9_.-]*|(?::[A-Za-z0-9_.-]+)+)$")) and
47
+ (.filter | type == "string" and test("^[A-Za-z0-9_.#$*-]+$")))) and
48
+ ([.targetTests[].filter] | length == (unique | length)) and
43
49
  (.deviceTestsRequired | type == "boolean") and
44
50
  (.testPolicy == "required" or .testPolicy == "not-required") and
45
51
  (if .testPolicy == "not-required" then (.testPolicyReason | type == "string" and length >= 20) else true end)
@@ -68,10 +74,15 @@ while IFS= read -r protected; do
68
74
  fi
69
75
  done < <(jq -r '.protectedPaths[]' "$AUTOMATION_CONFIG")
70
76
 
71
- safe_filter_pattern='^[A-Za-z0-9_.#$*-]+$'
72
- while IFS= read -r filter; do
73
- [[ "$filter" =~ $safe_filter_pattern ]] || automation_die "unsafe target test filter: $filter"
74
- done < <(jq -r '.targetTests[]' "$contract")
77
+ while IFS=$'\t' read -r gradle_task filter; do
78
+ automation_validate_gradle_task "$gradle_task"
79
+ automation_validate_test_filter "$filter"
80
+ jq -e \
81
+ --arg task "$gradle_task" \
82
+ '.gradleVerification.focusedTestTasks | index($task) != null' \
83
+ "$AUTOMATION_CONFIG" >/dev/null || \
84
+ automation_die "target test Gradle task is not allowed by automation/config.json: $gradle_task"
85
+ done < <(jq -r '.targetTests[] | [.gradleTask, .filter] | @tsv' "$contract")
75
86
 
76
87
  if rg -n -i 'replace with|TASK-EXAMPLE|todo|tbd|placeholder' "$contract" >/dev/null; then
77
88
  automation_die "contract still contains template placeholders"
@@ -12,26 +12,27 @@ head="${3:-}"
12
12
  automation_validate_task_id "$task_id"
13
13
  [[ "$(automation_read_state "$task_id")" == "INTEGRATING" ]] || automation_die "$task_id is not INTEGRATING"
14
14
  contract="$(automation_contract_path "$task_id")"
15
+ "$SCRIPT_DIR/validate-contract.sh" "$task_id" >/dev/null
15
16
 
16
17
  "$SCRIPT_DIR/integration-scope-gate.sh" "$task_id" "$base" "$head"
17
18
 
18
- while IFS= read -r filter; do
19
- automation_info "running integration focused test: $filter"
20
- ./gradlew testDebugUnitTest --tests "$filter"
21
- done < <(jq -r '.targetTests[]' "$contract")
19
+ while IFS=$'\t' read -r gradle_task filter; do
20
+ automation_info "running integration focused test ($gradle_task): $filter"
21
+ automation_run_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT"
22
+ done < <(jq -r '.targetTests[] | [.gradleTask, .filter] | @tsv' "$contract")
22
23
 
23
24
  automation_info "running integration full unit tests"
24
- ./gradlew testDebugUnitTest
25
+ automation_run_gradle_group "fullUnitTestTasks" "$AUTOMATION_ROOT"
25
26
 
26
- automation_info "building integration debug APK"
27
- ./gradlew assembleDebug
27
+ automation_info "running configured integration assemble tasks"
28
+ automation_run_gradle_group "assembleTasks" "$AUTOMATION_ROOT"
28
29
 
29
- automation_info "running integration Android lint"
30
- ./gradlew lint
30
+ automation_info "running configured integration Android lint tasks"
31
+ automation_run_gradle_group "lintTasks" "$AUTOMATION_ROOT"
31
32
 
32
33
  if [[ "$(jq -r '.deviceTestsRequired' "$contract")" == "true" ]]; then
33
- automation_info "running integration connected device tests"
34
- ./gradlew connectedDebugAndroidTest
34
+ automation_info "running configured integration device tests"
35
+ automation_run_gradle_group "deviceTestTasks" "$AUTOMATION_ROOT"
35
36
  fi
36
37
 
37
38
  automation_info "integration candidate passed all deterministic verification"
@@ -17,38 +17,23 @@ evidence_dir="$(automation_evidence_path "$task_id")"
17
17
 
18
18
  "$SCRIPT_DIR/scope-gate.sh" "$task_id"
19
19
 
20
- while IFS= read -r filter; do
21
- automation_info "running focused test: $filter"
22
- (
23
- cd "$AUTOMATION_ROOT"
24
- ./gradlew testDebugUnitTest --tests "$filter"
25
- )
26
- done < <(jq -r '.targetTests[]' "$contract")
20
+ while IFS=$'\t' read -r gradle_task filter; do
21
+ automation_info "running focused test ($gradle_task): $filter"
22
+ automation_run_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT"
23
+ done < <(jq -r '.targetTests[] | [.gradleTask, .filter] | @tsv' "$contract")
27
24
 
28
25
  automation_info "running full unit tests"
29
- (
30
- cd "$AUTOMATION_ROOT"
31
- ./gradlew testDebugUnitTest
32
- )
33
-
34
- automation_info "building debug APK"
35
- (
36
- cd "$AUTOMATION_ROOT"
37
- ./gradlew assembleDebug
38
- )
39
-
40
- automation_info "running Android lint"
41
- (
42
- cd "$AUTOMATION_ROOT"
43
- ./gradlew lint
44
- )
26
+ automation_run_gradle_group "fullUnitTestTasks" "$AUTOMATION_ROOT"
27
+
28
+ automation_info "running configured assemble tasks"
29
+ automation_run_gradle_group "assembleTasks" "$AUTOMATION_ROOT"
30
+
31
+ automation_info "running configured Android lint tasks"
32
+ automation_run_gradle_group "lintTasks" "$AUTOMATION_ROOT"
45
33
 
46
34
  if [[ "$(jq -r '.deviceTestsRequired' "$contract")" == "true" ]]; then
47
- automation_info "running connected device tests"
48
- (
49
- cd "$AUTOMATION_ROOT"
50
- ./gradlew connectedDebugAndroidTest
51
- )
35
+ automation_info "running configured device tests"
36
+ automation_run_gradle_group "deviceTestTasks" "$AUTOMATION_ROOT"
52
37
  fi
53
38
 
54
39
  automation_info "all deterministic verification commands passed"