@frankzhang2026/opencode-android-orchestrator 0.1.0 → 0.3.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 (124) hide show
  1. package/CHANGELOG.md +92 -2
  2. package/LICENSE +21 -0
  3. package/README.md +402 -6
  4. package/THIRD_PARTY_NOTICES.md +119 -0
  5. package/dist/cli.js +360 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/compatibility/hooks.d.ts +23 -0
  8. package/dist/compatibility/hooks.d.ts.map +1 -0
  9. package/dist/compatibility/hooks.js +44 -0
  10. package/dist/compatibility/hooks.js.map +1 -0
  11. package/dist/compatibility/versions.d.ts +19 -0
  12. package/dist/compatibility/versions.d.ts.map +1 -1
  13. package/dist/compatibility/versions.js +116 -0
  14. package/dist/compatibility/versions.js.map +1 -1
  15. package/dist/doctor/index.d.ts +36 -0
  16. package/dist/doctor/index.d.ts.map +1 -0
  17. package/dist/doctor/index.js +289 -0
  18. package/dist/doctor/index.js.map +1 -0
  19. package/dist/doctor/installation.d.ts +4 -0
  20. package/dist/doctor/installation.d.ts.map +1 -0
  21. package/dist/doctor/installation.js +497 -0
  22. package/dist/doctor/installation.js.map +1 -0
  23. package/dist/index.d.ts +13 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +13 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/installer/adaptive-templates.d.ts +78 -0
  28. package/dist/installer/adaptive-templates.d.ts.map +1 -0
  29. package/dist/installer/adaptive-templates.js +233 -0
  30. package/dist/installer/adaptive-templates.js.map +1 -0
  31. package/dist/installer/agents-config.d.ts +19 -0
  32. package/dist/installer/agents-config.d.ts.map +1 -0
  33. package/dist/installer/agents-config.js +147 -0
  34. package/dist/installer/agents-config.js.map +1 -0
  35. package/dist/installer/android-project.d.ts +36 -0
  36. package/dist/installer/android-project.d.ts.map +1 -0
  37. package/dist/installer/android-project.js +418 -0
  38. package/dist/installer/android-project.js.map +1 -0
  39. package/dist/installer/index.d.ts +8 -0
  40. package/dist/installer/index.d.ts.map +1 -1
  41. package/dist/installer/index.js +8 -1
  42. package/dist/installer/index.js.map +1 -1
  43. package/dist/installer/init.d.ts +68 -0
  44. package/dist/installer/init.d.ts.map +1 -0
  45. package/dist/installer/init.js +398 -0
  46. package/dist/installer/init.js.map +1 -0
  47. package/dist/installer/install-manifest.d.ts +132 -0
  48. package/dist/installer/install-manifest.d.ts.map +1 -0
  49. package/dist/installer/install-manifest.js +1047 -0
  50. package/dist/installer/install-manifest.js.map +1 -0
  51. package/dist/installer/opencode-config.d.ts +32 -0
  52. package/dist/installer/opencode-config.d.ts.map +1 -0
  53. package/dist/installer/opencode-config.js +302 -0
  54. package/dist/installer/opencode-config.js.map +1 -0
  55. package/dist/installer/uninstall.d.ts +70 -0
  56. package/dist/installer/uninstall.d.ts.map +1 -0
  57. package/dist/installer/uninstall.js +770 -0
  58. package/dist/installer/uninstall.js.map +1 -0
  59. package/dist/installer/upgrade.d.ts +101 -0
  60. package/dist/installer/upgrade.d.ts.map +1 -0
  61. package/dist/installer/upgrade.js +1140 -0
  62. package/dist/installer/upgrade.js.map +1 -0
  63. package/dist/opencode-plugin.d.ts +2 -0
  64. package/dist/opencode-plugin.d.ts.map +1 -0
  65. package/dist/opencode-plugin.js +4 -0
  66. package/dist/opencode-plugin.js.map +1 -0
  67. package/dist/plugin/index.d.ts +7 -2
  68. package/dist/plugin/index.d.ts.map +1 -1
  69. package/dist/plugin/index.js +23 -6
  70. package/dist/plugin/index.js.map +1 -1
  71. package/dist/tools/index.d.ts +35 -1
  72. package/dist/tools/index.d.ts.map +1 -1
  73. package/dist/tools/index.js +197 -1
  74. package/dist/tools/index.js.map +1 -1
  75. package/docs/MIGRATION.md +184 -0
  76. package/docs/SECURITY.md +219 -0
  77. package/docs/TROUBLESHOOTING.md +171 -0
  78. package/package.json +23 -5
  79. package/templates/.opencode/agents/scheduled-coder.md +115 -0
  80. package/templates/.opencode/agents/scheduled-planner.md +166 -0
  81. package/templates/.opencode/agents/scheduled-reviewer.md +88 -0
  82. package/templates/.opencode/commands/abort-task.md +20 -0
  83. package/templates/.opencode/commands/acceptance.md +20 -0
  84. package/templates/.opencode/commands/change.md +23 -0
  85. package/templates/.opencode/commands/resume-review.md +19 -0
  86. package/templates/.opencode/skills/scheduled-quality-coder/SKILL.md +93 -0
  87. package/templates/.opencode/skills/scheduled-quality-orchestrator/SKILL.md +216 -0
  88. package/templates/.opencode/skills/scheduled-quality-reviewer/SKILL.md +68 -0
  89. package/templates/AGENTS.md.fragment +19 -0
  90. package/templates/README.md +55 -9
  91. package/templates/automation/config.json +67 -0
  92. package/templates/automation/config.schema.json +194 -0
  93. package/templates/automation/task-contract.schema.json +92 -0
  94. package/templates/automation/tasks/TASK-TEMPLATE.json.example +56 -0
  95. package/templates/docs/plans/README.md +26 -0
  96. package/templates/installation-manifest.schema.json +201 -0
  97. package/templates/scripts/automation/abort-task.sh +171 -0
  98. package/templates/scripts/automation/accept-and-integrate.sh +263 -0
  99. package/templates/scripts/automation/acceptance-report.sh +132 -0
  100. package/templates/scripts/automation/approve-and-run.sh +175 -0
  101. package/templates/scripts/automation/begin-review.sh +18 -0
  102. package/templates/scripts/automation/block-task.sh +26 -0
  103. package/templates/scripts/automation/claim-task.sh +74 -0
  104. package/templates/scripts/automation/integration-scope-gate.sh +103 -0
  105. package/templates/scripts/automation/lib.sh +815 -0
  106. package/templates/scripts/automation/orchestrate-task.sh +119 -0
  107. package/templates/scripts/automation/preflight.sh +206 -0
  108. package/templates/scripts/automation/prepare-contract-review.sh +101 -0
  109. package/templates/scripts/automation/quality-gate.sh +72 -0
  110. package/templates/scripts/automation/queue-task.sh +35 -0
  111. package/templates/scripts/automation/record-red.sh +55 -0
  112. package/templates/scripts/automation/resume-review-fix.sh +32 -0
  113. package/templates/scripts/automation/resume-review.sh +120 -0
  114. package/templates/scripts/automation/scope-gate.sh +110 -0
  115. package/templates/scripts/automation/select-task.sh +34 -0
  116. package/templates/scripts/automation/shadow-run.sh +24 -0
  117. package/templates/scripts/automation/show-acceptance-review.sh +111 -0
  118. package/templates/scripts/automation/status.sh +125 -0
  119. package/templates/scripts/automation/submit-review.sh +73 -0
  120. package/templates/scripts/automation/tests/run-tests.sh +716 -0
  121. package/templates/scripts/automation/transition-state.sh +21 -0
  122. package/templates/scripts/automation/validate-contract.sh +98 -0
  123. package/templates/scripts/automation/verify-integration.sh +38 -0
  124. package/templates/scripts/automation/verify-task.sh +39 -0
@@ -0,0 +1,716 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ TEST_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ SOURCE_SCRIPT_DIR="$(cd "$TEST_DIR/.." && pwd)"
6
+ fixture="$(mktemp -d "${TMPDIR:-/tmp}/scheduled-quality-gate-tests.XXXXXX")"
7
+ runtime_root="$(mktemp -d "${TMPDIR:-/tmp}/scheduled-quality-runtime.XXXXXX")"
8
+ worktree_base="$(mktemp -d "${TMPDIR:-/tmp}/scheduled-quality-worktrees.XXXXXX")"
9
+ fixture="$(cd "$fixture" && pwd -P)"
10
+ runtime_root="$(cd "$runtime_root" && pwd -P)"
11
+ worktree_base="$(cd "$worktree_base" && pwd -P)"
12
+
13
+ cleanup() {
14
+ if [[ -d "$fixture/.git" ]]; then
15
+ while IFS= read -r path; do
16
+ [[ -n "$path" && "$path" != "$fixture" ]] || continue
17
+ git -C "$fixture" worktree remove --force "$path" >/dev/null 2>&1 || true
18
+ done < <(git -C "$fixture" worktree list --porcelain 2>/dev/null | awk '/^worktree / { sub(/^worktree /, ""); print }')
19
+ fi
20
+ rm -rf "$fixture" "$runtime_root" "$worktree_base"
21
+ }
22
+ trap cleanup EXIT
23
+
24
+ pass_count=0
25
+
26
+ pass() {
27
+ pass_count=$((pass_count + 1))
28
+ printf 'ok %d - %s\n' "$pass_count" "$1"
29
+ }
30
+
31
+ fail() {
32
+ printf 'not ok - %s\n' "$1" >&2
33
+ exit 1
34
+ }
35
+
36
+ run_fixture() {
37
+ (
38
+ cd "$fixture"
39
+ unset AUTOMATION_PROJECT_ROOT
40
+ export AUTOMATION_TEST_MODE=1
41
+ export AUTOMATION_RUNTIME_ROOT="$runtime_root"
42
+ export AUTOMATION_WORKTREE_BASE="$worktree_base"
43
+ "$@"
44
+ )
45
+ }
46
+
47
+ run_task() {
48
+ local task_root="$1"
49
+ shift
50
+ (
51
+ cd "$task_root"
52
+ unset AUTOMATION_PROJECT_ROOT
53
+ export AUTOMATION_TEST_MODE=1
54
+ export AUTOMATION_RUNTIME_ROOT="$runtime_root"
55
+ export AUTOMATION_WORKTREE_BASE="$worktree_base"
56
+ "$@"
57
+ )
58
+ }
59
+
60
+ write_workspace() {
61
+ local task_id="$1"
62
+ local head branch
63
+ head="$(git -C "$fixture" rev-parse HEAD)"
64
+ branch="$(git -C "$fixture" symbolic-ref --short HEAD)"
65
+ mkdir -p "$runtime_root/workspaces"
66
+ jq -n \
67
+ --arg taskId "$task_id" \
68
+ --arg sourceRoot "$fixture" \
69
+ --arg originalBranch "$branch" \
70
+ --arg baselineHead "$head" \
71
+ --arg taskRoot "$fixture" \
72
+ --arg taskBranch "$branch" \
73
+ --arg worktreeBase "$worktree_base" \
74
+ '{taskId: $taskId, sourceRoot: $sourceRoot,
75
+ originalBranch: $originalBranch, baselineHead: $baselineHead,
76
+ workspaceStrategy: "inPlaceExclusive", taskRoot: $taskRoot,
77
+ repositoryLeaseRequired: false, taskBranch: $taskBranch,
78
+ worktreeBase: $worktreeBase, codingCycle: 0, reviewCycles: 0}' \
79
+ > "$runtime_root/workspaces/$task_id.json"
80
+ }
81
+
82
+ mkdir -p \
83
+ "$fixture/automation/tasks" \
84
+ "$fixture/docs/plans" \
85
+ "$fixture/scripts/automation" \
86
+ "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture" \
87
+ "$fixture/mobile-client/src/test/java/dev/example/orchestratorfixture"
88
+
89
+ cp "$SOURCE_SCRIPT_DIR"/*.sh "$fixture/scripts/automation/"
90
+ chmod +x "$fixture/scripts/automation/"*.sh
91
+ printf '%s\n' '# Approved test plan' > "$fixture/docs/plans/TASK-TEST-001.md"
92
+ printf '%s\n' 'class ExistingFeature' > "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/ExistingFeature.kt"
93
+
94
+ printf '%s\n' \
95
+ '#!/usr/bin/env bash' \
96
+ 'set -euo pipefail' \
97
+ 'task="${1:-}"' \
98
+ 'if [[ "$task" == "testDebugUnitTest" && "${2:-}" == "--tests" && "${AUTOMATION_FAKE_GREEN:-0}" != "1" ]]; then' \
99
+ ' printf "%s\n" "expected missing behavior"' \
100
+ ' exit 1' \
101
+ 'fi' \
102
+ 'case "$task" in' \
103
+ ' testDebugUnitTest|assembleDebug|lint|connectedDebugAndroidTest) printf "%s\n" "BUILD SUCCESSFUL" ;;' \
104
+ ' *) printf "%s\n" "unexpected Gradle task: $task" >&2; exit 9 ;;' \
105
+ 'esac' \
106
+ > "$fixture/gradlew"
107
+ chmod +x "$fixture/gradlew"
108
+
109
+ jq -n '{
110
+ schemaVersion: 3,
111
+ enabled: true,
112
+ mode: "orchestrated",
113
+ workspaceStrategy: "inPlaceExclusive",
114
+ originalBranchDriftPolicy: "block",
115
+ worktreeBase: "",
116
+ maxFixLoops: 1,
117
+ maxReviewCycles: 1,
118
+ maxReviewerRestarts: 2,
119
+ autoCleanupWorktrees: true,
120
+ pushAfterAcceptance: false,
121
+ approvalPhrases: {
122
+ proposal: "批准方案,生成计划和任务合同。",
123
+ contract: "合同已复核,批准自动执行到人工验收阶段。",
124
+ acceptance: "验收通过,提交到原分支。",
125
+ abort: "中止任务,封存修改并恢复原分支。"
126
+ },
127
+ plugins: {
128
+ superpowers: "superpowers@git+https://github.com/obra/superpowers.git#v6.2.0"
129
+ },
130
+ requiredSkills: [
131
+ "brainstorming",
132
+ "writing-plans",
133
+ "scheduled-quality-orchestrator",
134
+ "scheduled-quality-coder",
135
+ "scheduled-quality-reviewer",
136
+ "test-driven-development",
137
+ "systematic-debugging",
138
+ "verification-before-completion"
139
+ ],
140
+ gradleVerification: {
141
+ fullUnitTestTasks: ["testDebugUnitTest"],
142
+ focusedTestTasks: ["testDebugUnitTest"],
143
+ assembleTasks: ["assembleDebug"],
144
+ lintTasks: ["lint"],
145
+ deviceTestTasks: ["connectedDebugAndroidTest"]
146
+ },
147
+ androidProject: {
148
+ name: "automation-shell-fixture",
149
+ gradleDsl: "kotlin",
150
+ settingsFile: "settings.gradle.kts",
151
+ moduleScope: "all",
152
+ primaryModule: ":mobile-client",
153
+ modules: [{
154
+ gradlePath: ":mobile-client",
155
+ directory: "mobile-client",
156
+ buildFile: "mobile-client/build.gradle.kts",
157
+ dsl: "kotlin",
158
+ type: "application",
159
+ namespace: "dev.example.orchestratorfixture",
160
+ applicationId: "dev.example.orchestratorfixture"
161
+ }],
162
+ productionPaths: ["mobile-client/src/main/**"],
163
+ testPaths: [
164
+ "mobile-client/src/test/**",
165
+ "mobile-client/src/androidTest/**"
166
+ ]
167
+ },
168
+ protectedPaths: [
169
+ ".opencode/",
170
+ ".automation-plugin/",
171
+ "automation/",
172
+ "scripts/automation/",
173
+ "opencode.json",
174
+ "opencode.jsonc",
175
+ "AGENTS.md",
176
+ "gradle/",
177
+ "gradlew",
178
+ "gradlew.bat",
179
+ "settings.gradle",
180
+ "settings.gradle.kts",
181
+ "build.gradle",
182
+ "build.gradle.kts",
183
+ "**/build.gradle",
184
+ "**/build.gradle.kts",
185
+ "mobile-client/build.gradle.kts"
186
+ ]
187
+ }' > "$fixture/automation/config.json"
188
+
189
+ jq -n '{
190
+ schemaVersion: 1,
191
+ id: "TASK-TEST-001",
192
+ title: "Add an observable greeting behavior",
193
+ designApproved: true,
194
+ planPath: "docs/plans/TASK-TEST-001.md",
195
+ ambiguityPolicy: "BLOCKED",
196
+ maxFixLoops: 1,
197
+ maxChangedFiles: 4,
198
+ allowedPaths: [
199
+ "mobile-client/src/main/java/dev/example/orchestratorfixture/**",
200
+ "mobile-client/src/test/java/dev/example/orchestratorfixture/**"
201
+ ],
202
+ forbiddenPaths: [
203
+ ".opencode/**",
204
+ ".automation-plugin/**",
205
+ "automation/**",
206
+ "scripts/automation/**",
207
+ "opencode.json",
208
+ "opencode.jsonc",
209
+ "AGENTS.md",
210
+ "gradle/**",
211
+ "gradlew",
212
+ "gradlew.bat",
213
+ "settings.gradle",
214
+ "settings.gradle.kts",
215
+ "build.gradle",
216
+ "build.gradle.kts",
217
+ "**/build.gradle",
218
+ "**/build.gradle.kts",
219
+ "mobile-client/build.gradle.kts"
220
+ ],
221
+ allowedSuperpowers: [
222
+ "test-driven-development",
223
+ "systematic-debugging",
224
+ "verification-before-completion"
225
+ ],
226
+ acceptanceCriteria: ["Greeting returns the approved value"],
227
+ nonGoals: ["No unrelated refactoring"],
228
+ targetTests: [{
229
+ gradleTask: "testDebugUnitTest",
230
+ filter: "dev.example.orchestratorfixture.GreetingTest"
231
+ }],
232
+ deviceTestsRequired: false,
233
+ testPolicy: "required",
234
+ testPolicyReason: "The behavior requires a focused regression test"
235
+ }' > "$fixture/automation/tasks/TASK-TEST-001.json"
236
+
237
+ (
238
+ cd "$fixture"
239
+ git init -q
240
+ git config user.email 'automation-tests@example.invalid'
241
+ git config user.name 'Automation Tests'
242
+ git add .
243
+ git commit -qm 'Create automation fixture'
244
+ )
245
+
246
+ run_fixture ./scripts/automation/validate-contract.sh TASK-TEST-001 >/dev/null
247
+ pass 'valid version-3 configuration and task contract are accepted'
248
+
249
+ if run_fixture env AUTOMATION_HUMAN_APPROVED=0 ./scripts/automation/queue-task.sh TASK-TEST-001 >/dev/null 2>&1; then
250
+ fail 'legacy queue accepted without explicit human approval'
251
+ fi
252
+ pass 'legacy queue rejects missing human approval'
253
+
254
+ run_fixture env AUTOMATION_HUMAN_APPROVED=1 ./scripts/automation/queue-task.sh TASK-TEST-001 >/dev/null
255
+ write_workspace TASK-TEST-001
256
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-001.json")" == "PENDING" ]] || fail 'queue did not create PENDING state'
257
+ pass 'explicit queue creates one auditable PENDING state'
258
+
259
+ [[ "$(run_fixture ./scripts/automation/select-task.sh PENDING)" == "TASK-TEST-001" ]] || fail 'selector did not return the only pending task'
260
+ pass 'selector returns exactly one eligible task'
261
+
262
+ jq '.taskId = "TASK-TEST-999"' \
263
+ "$runtime_root/state/TASK-TEST-001.json" \
264
+ > "$runtime_root/state/TASK-TEST-999.json"
265
+ if run_fixture ./scripts/automation/select-task.sh PENDING >/dev/null 2>&1; then
266
+ fail 'selector chose among multiple pending tasks'
267
+ fi
268
+ rm "$runtime_root/state/TASK-TEST-999.json"
269
+ pass 'selector refuses to choose among multiple eligible tasks'
270
+
271
+ mkdir -p "$runtime_root/locks/TASK-TEST-001.state.lock"
272
+ printf '%s\n' '99999999' > "$runtime_root/locks/TASK-TEST-001.state.lock/pid"
273
+ if run_fixture ./scripts/automation/transition-state.sh TASK-TEST-001 PENDING READY_FOR_REVIEW human invalid >/dev/null 2>&1; then
274
+ fail 'illegal transition was accepted'
275
+ fi
276
+ [[ ! -d "$runtime_root/locks/TASK-TEST-001.state.lock" ]] || fail 'stale state lock was not recovered'
277
+ pass 'stopped-process locks are recovered without bypassing state rules'
278
+
279
+ if run_fixture ./scripts/automation/transition-state.sh TASK-TEST-001 PENDING READY_FOR_REVIEW human invalid >/dev/null 2>&1; then
280
+ fail 'illegal transition was accepted after lock recovery'
281
+ fi
282
+ pass 'illegal state transition is rejected'
283
+
284
+ run_fixture ./scripts/automation/claim-task.sh TASK-TEST-001 >/dev/null
285
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-001.json")" == "CODING" ]] || fail 'claim did not create CODING state'
286
+ [[ -f "$runtime_root/evidence/TASK-TEST-001/baseline.json" ]] || fail 'baseline metadata missing'
287
+ jq -e \
288
+ '.command == ["./gradlew", "testDebugUnitTest"]' \
289
+ "$runtime_root/evidence/TASK-TEST-001/baseline.json" >/dev/null || fail 'baseline command does not match the configured task matrix'
290
+ pass 'claim verifies workspace identity and records a green baseline'
291
+
292
+ printf '%s\n' 'class GreetingTest { fun expectedBehavior() = Unit }' > "$fixture/mobile-client/src/test/java/dev/example/orchestratorfixture/GreetingTest.kt"
293
+ run_fixture ./scripts/automation/record-red.sh TASK-TEST-001 'expected missing behavior' -- dev.example.orchestratorfixture.GreetingTest >/dev/null
294
+ [[ "$(jq -r '.exitCode' "$runtime_root/evidence/TASK-TEST-001/red.json")" -ne 0 ]] || fail 'RED evidence exit code was not captured'
295
+ jq -e \
296
+ '.command == ["./gradlew", "testDebugUnitTest", "--tests", "dev.example.orchestratorfixture.GreetingTest"]' \
297
+ "$runtime_root/evidence/TASK-TEST-001/red.json" >/dev/null || fail 'RED command does not bind the configured task and filter'
298
+ pass 'focused failing test records genuine RED evidence'
299
+
300
+ printf '%s\n' 'unsafe' > "$fixture/automation/forbidden-change.txt"
301
+ if run_fixture ./scripts/automation/scope-gate.sh TASK-TEST-001 >/dev/null 2>&1; then
302
+ fail 'scope gate accepted protected path change'
303
+ fi
304
+ rm "$fixture/automation/forbidden-change.txt"
305
+ pass 'scope gate rejects protected path changes'
306
+
307
+ printf '%s\n' 'class GreetingTest { fun expectedBehavior() { assertTrue(true) } }' > "$fixture/mobile-client/src/test/java/dev/example/orchestratorfixture/GreetingTest.kt"
308
+ printf '%s\n' 'class Greeting { fun value() = "hello" }' > "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/Greeting.kt"
309
+ if run_fixture ./scripts/automation/scope-gate.sh TASK-TEST-001 >/dev/null 2>&1; then
310
+ fail 'scope gate accepted weakened test'
311
+ fi
312
+ pass 'scope gate rejects obvious test weakening'
313
+
314
+ printf '%s\n' 'class GreetingTest { fun expectedBehavior() { check(Greeting().value() == "hello") } }' > "$fixture/mobile-client/src/test/java/dev/example/orchestratorfixture/GreetingTest.kt"
315
+ run_fixture env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/quality-gate.sh TASK-TEST-001 >/dev/null
316
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-001.json")" == "READY_FOR_REVIEW" ]] || fail 'quality gate did not create READY_FOR_REVIEW state'
317
+ pass 'G1-G6 seal all tracked and untracked product changes'
318
+
319
+ run_fixture ./scripts/automation/begin-review.sh TASK-TEST-001 >/dev/null
320
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-001.json")" == "REVIEWING" ]] || fail 'review handoff did not create REVIEWING state'
321
+ review_status="$(run_fixture ./scripts/automation/status.sh TASK-TEST-001)"
322
+ [[ "$(jq -r '.evidence.red.exitCode' <<< "$review_status")" -ne 0 ]] || fail 'review status omitted RED evidence'
323
+ [[ "$(jq -r '.evidence.latestGate.lastExitCode' <<< "$review_status")" -eq 0 ]] || fail 'review status omitted gate evidence'
324
+ [[ "$(jq -r '.evidence.sealedDiffMatches' <<< "$review_status")" == "true" ]] || fail 'review status did not verify the sealed diff'
325
+ pass 'orchestrator creates an explicit sealed reviewer handoff'
326
+
327
+ run_fixture ./scripts/automation/transition-state.sh \
328
+ TASK-TEST-001 REVIEWING BLOCKED orchestrator \
329
+ 'reviewer exited without submitting a decision' >/dev/null
330
+ sealed_sha_before_resume="$(jq -r '.diffSha256' "$runtime_root/evidence/TASK-TEST-001/ready.json")"
331
+ run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/resume-review.sh TASK-TEST-001 >/dev/null
332
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-001.json")" == "REVIEWING" ]] || fail 'reviewer-only recovery did not return directly to REVIEWING'
333
+ [[ "$(jq -r '.codingCycle' "$runtime_root/workspaces/TASK-TEST-001.json")" -eq 0 ]] || fail 'reviewer-only recovery consumed a coding cycle'
334
+ [[ "$(jq -r '.reviewCycles' "$runtime_root/workspaces/TASK-TEST-001.json")" -eq 0 ]] || fail 'reviewer-only recovery consumed a repair cycle'
335
+ [[ "$(jq -r '.diffSha256' "$runtime_root/evidence/TASK-TEST-001/ready.json")" == "$sealed_sha_before_resume" ]] || fail 'reviewer-only recovery changed ready evidence'
336
+ [[ "$(jq -r '.coderRerun' "$runtime_root/evidence/TASK-TEST-001/review-resumptions.jsonl")" == "false" ]] || fail 'reviewer-only recovery did not audit the coder bypass'
337
+ pass 'a budget-exhausted reviewer resumes from the sealed diff without rerunning Coder'
338
+
339
+ run_fixture env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/submit-review.sh TASK-TEST-001 APPROVED 'Independent diff review found no material issue.' >/dev/null
340
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-001.json")" == "AWAITING_HUMAN" ]] || fail 'review did not create AWAITING_HUMAN state'
341
+ pass 'independent approval stops at AWAITING_HUMAN'
342
+
343
+ transition_count="$(wc -l < "$runtime_root/evidence/TASK-TEST-001/transitions.jsonl" | tr -d ' ')"
344
+ [[ "$transition_count" -eq 8 ]] || fail "unexpected transition audit count: $transition_count"
345
+ pass 'append-only transition audit contains the complete gated lifecycle'
346
+
347
+ (
348
+ cd "$fixture"
349
+ git add mobile-client
350
+ git commit -qm 'Preserve first fixture task'
351
+ )
352
+
353
+ printf '%s\n' '# Approved failure-loop plan' > "$fixture/docs/plans/TASK-TEST-002.md"
354
+ jq \
355
+ '.id = "TASK-TEST-002" |
356
+ .title = "Exercise the bounded verification failure loop" |
357
+ .planPath = "docs/plans/TASK-TEST-002.md" |
358
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.FailureLoopTest"}]' \
359
+ "$fixture/automation/tasks/TASK-TEST-001.json" \
360
+ > "$fixture/automation/tasks/TASK-TEST-002.json"
361
+ (
362
+ cd "$fixture"
363
+ git add automation/tasks/TASK-TEST-002.json docs/plans/TASK-TEST-002.md
364
+ git commit -qm 'Add second approved fixture task'
365
+ )
366
+ run_fixture env AUTOMATION_HUMAN_APPROVED=1 ./scripts/automation/queue-task.sh TASK-TEST-002 >/dev/null
367
+ write_workspace TASK-TEST-002
368
+ run_fixture ./scripts/automation/claim-task.sh TASK-TEST-002 >/dev/null
369
+ printf '%s\n' 'class FailureLoopTest { fun expectedBehavior() = Unit }' > "$fixture/mobile-client/src/test/java/dev/example/orchestratorfixture/FailureLoopTest.kt"
370
+ run_fixture ./scripts/automation/record-red.sh TASK-TEST-002 'expected missing behavior' -- dev.example.orchestratorfixture.FailureLoopTest >/dev/null
371
+ printf '%s\n' 'class FailureLoop { fun value() = "still failing" }' > "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/FailureLoop.kt"
372
+
373
+ if run_fixture ./scripts/automation/quality-gate.sh TASK-TEST-002 >/dev/null 2>&1; then
374
+ fail 'first failing gate attempt unexpectedly passed'
375
+ fi
376
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-002.json")" == "CODING" ]] || fail 'first failure did not preserve CODING'
377
+ pass 'first verification failure allows one systematic fix attempt'
378
+
379
+ if run_fixture ./scripts/automation/quality-gate.sh TASK-TEST-002 >/dev/null 2>&1; then
380
+ fail 'second failing gate attempt unexpectedly passed'
381
+ fi
382
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-002.json")" == "TEST_FAILED" ]] || fail 'second failure did not create TEST_FAILED'
383
+ pass 'second verification failure deterministically stops in TEST_FAILED'
384
+
385
+ (
386
+ cd "$fixture"
387
+ git add mobile-client
388
+ git commit -qm 'Preserve second fixture outcome'
389
+ )
390
+ printf '%s\n' '# Approved preflight-block plan' > "$fixture/docs/plans/TASK-TEST-003.md"
391
+ jq \
392
+ '.id = "TASK-TEST-003" |
393
+ .title = "Verify dirty worktree preflight blocking" |
394
+ .planPath = "docs/plans/TASK-TEST-003.md" |
395
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.PreflightBlockTest"}]' \
396
+ "$fixture/automation/tasks/TASK-TEST-001.json" \
397
+ > "$fixture/automation/tasks/TASK-TEST-003.json"
398
+ (
399
+ cd "$fixture"
400
+ git add automation/tasks/TASK-TEST-003.json docs/plans/TASK-TEST-003.md
401
+ git commit -qm 'Add preflight-block fixture task'
402
+ )
403
+ run_fixture env AUTOMATION_HUMAN_APPROVED=1 ./scripts/automation/queue-task.sh TASK-TEST-003 >/dev/null
404
+ write_workspace TASK-TEST-003
405
+ printf '%s\n' 'dirty' > "$fixture/unapproved-change.txt"
406
+ if run_fixture ./scripts/automation/claim-task.sh TASK-TEST-003 >/dev/null 2>&1; then
407
+ fail 'claim accepted a dirty worktree'
408
+ fi
409
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-003.json")" == "BLOCKED" ]] || fail 'preflight failure did not create BLOCKED'
410
+ if run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/resume-review.sh TASK-TEST-003 >/dev/null 2>&1; then
411
+ fail 'reviewer-only recovery accepted a non-review blocker'
412
+ fi
413
+ rm "$fixture/unapproved-change.txt"
414
+ pass 'preflight blocks dirty task roots without retrying implicitly'
415
+ pass 'reviewer-only recovery rejects unrelated BLOCKED states'
416
+
417
+ printf '%s\n' '# Approved reviewer-fix plan' > "$fixture/docs/plans/TASK-TEST-005.md"
418
+ jq \
419
+ '.id = "TASK-TEST-005" |
420
+ .title = "Exercise the bounded reviewer repair cycle" |
421
+ .planPath = "docs/plans/TASK-TEST-005.md" |
422
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.ReviewerFixTest"}]' \
423
+ "$fixture/automation/tasks/TASK-TEST-001.json" \
424
+ > "$fixture/automation/tasks/TASK-TEST-005.json"
425
+ (
426
+ cd "$fixture"
427
+ git add automation/tasks/TASK-TEST-005.json docs/plans/TASK-TEST-005.md
428
+ git commit -qm 'Add reviewer-fix fixture task'
429
+ )
430
+ run_fixture env AUTOMATION_HUMAN_APPROVED=1 ./scripts/automation/queue-task.sh TASK-TEST-005 >/dev/null
431
+ write_workspace TASK-TEST-005
432
+ run_fixture ./scripts/automation/claim-task.sh TASK-TEST-005 >/dev/null
433
+ printf '%s\n' 'class ReviewerFixTest { fun expectedBehavior() = Unit }' > "$fixture/mobile-client/src/test/java/dev/example/orchestratorfixture/ReviewerFixTest.kt"
434
+ run_fixture ./scripts/automation/record-red.sh TASK-TEST-005 'expected missing behavior' -- dev.example.orchestratorfixture.ReviewerFixTest >/dev/null
435
+ printf '%s\n' 'class ReviewerFix { fun value() = "first pass" }' > "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/ReviewerFix.kt"
436
+ run_fixture env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/quality-gate.sh TASK-TEST-005 >/dev/null
437
+ run_fixture ./scripts/automation/begin-review.sh TASK-TEST-005 >/dev/null
438
+ if run_fixture ./scripts/automation/submit-review.sh TASK-TEST-005 CHANGES_REQUESTED 'Reviewer found one material in-contract behavior issue.' >/dev/null 2>&1; then
439
+ fail 'changes-requested review unexpectedly returned success'
440
+ fi
441
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-005.json")" == "CHANGES_REQUESTED" ]] || fail 'review finding did not create CHANGES_REQUESTED'
442
+ pass 'reviewer findings are recorded as a distinct state'
443
+
444
+ run_fixture ./scripts/automation/resume-review-fix.sh TASK-TEST-005 >/dev/null
445
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-005.json")" == "CODING" ]] || fail 'bounded repair did not return to CODING'
446
+ [[ "$(jq -r '.codingCycle' "$runtime_root/workspaces/TASK-TEST-005.json")" -eq 1 ]] || fail 'repair did not create a fresh gate cycle'
447
+ pass 'one configured reviewer repair gets a fresh quality-gate cycle'
448
+
449
+ printf '%s\n' 'class ReviewerFix { fun value() = "second pass" }' > "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/ReviewerFix.kt"
450
+ run_fixture env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/quality-gate.sh TASK-TEST-005 >/dev/null
451
+ run_fixture ./scripts/automation/begin-review.sh TASK-TEST-005 >/dev/null
452
+ if run_fixture ./scripts/automation/submit-review.sh TASK-TEST-005 CHANGES_REQUESTED 'Fresh reviewer still found a material behavior issue.' >/dev/null 2>&1; then
453
+ fail 'second changes-requested review unexpectedly returned success'
454
+ fi
455
+ if run_fixture ./scripts/automation/resume-review-fix.sh TASK-TEST-005 >/dev/null 2>&1; then
456
+ fail 'review repair limit was bypassed'
457
+ fi
458
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-005.json")" == "NEEDS_HUMAN" ]] || fail 'repair exhaustion did not create NEEDS_HUMAN'
459
+ pass 'reviewer repair limit prevents an unbounded agent loop'
460
+
461
+ (
462
+ cd "$fixture"
463
+ git add mobile-client
464
+ git commit -qm 'Preserve reviewer-fix fixture outcome'
465
+ )
466
+
467
+ printf '%s\n' '# End-to-end orchestrated plan' > "$fixture/docs/plans/TASK-TEST-004.md"
468
+ jq \
469
+ '.id = "TASK-TEST-004" |
470
+ .title = "Exercise in-place transactional integration" |
471
+ .planPath = "docs/plans/TASK-TEST-004.md" |
472
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.OrchestratedFlowTest"}]' \
473
+ "$fixture/automation/tasks/TASK-TEST-001.json" \
474
+ > "$fixture/automation/tasks/TASK-TEST-004.json"
475
+
476
+ if run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-004 '确认' >/dev/null 2>&1; then
477
+ fail 'proposal preparation accepted an unbound confirmation'
478
+ fi
479
+ pass 'proposal gate requires the configured approval option token'
480
+
481
+ run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-004 '批准方案,生成计划和任务合同。' >/dev/null
482
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-004.json")" == "CONTRACT_REVIEW" ]] || fail 'proposal gate did not enter CONTRACT_REVIEW'
483
+ pass 'approved proposal seals plan, contract, branch, HEAD, and hashes'
484
+
485
+ if run_fixture ./scripts/automation/approve-and-run.sh TASK-TEST-004 '确认执行' >/dev/null 2>&1; then
486
+ fail 'contract execution accepted an unbound confirmation'
487
+ fi
488
+ pass 'contract gate rejects execution without the exact approval option token'
489
+
490
+ run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/approve-and-run.sh TASK-TEST-004 '合同已复核,批准自动执行到人工验收阶段。' >/dev/null
491
+ task_root="$(jq -er '.taskRoot' "$runtime_root/workspaces/TASK-TEST-004.json")"
492
+ approved_baseline="$(jq -er '.baselineHead' "$runtime_root/workspaces/TASK-TEST-004.json")"
493
+ [[ "$task_root" == "$fixture" ]] || fail 'in-place strategy did not keep the task in the source directory'
494
+ [[ "$(jq -r '.workspaceStrategy' "$runtime_root/workspaces/TASK-TEST-004.json")" == "inPlaceExclusive" ]] || fail 'workspace strategy was not recorded'
495
+ [[ "$(git -C "$fixture" symbolic-ref --short HEAD)" == "automation/task-test-004" ]] || fail 'source directory did not switch to the task branch'
496
+ [[ "$(git -C "$fixture" rev-list --count "$approved_baseline..automation/task-test-004")" -eq 0 ]] || fail 'contract approval created a planning-only commit'
497
+ [[ "$(jq -r '.contractCommit' "$runtime_root/evidence/TASK-TEST-004/origin.json")" == "null" ]] || fail 'contract approval recorded a commit before product integration'
498
+ [[ "$(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'
499
+ [[ -f "$runtime_root/locks/repository.workspace.lease/lease.json" ]] || fail 'persistent repository lease was not created'
500
+ leased_status="$(run_fixture ./scripts/automation/status.sh TASK-TEST-004)"
501
+ [[ "$(jq -r '.runtime.repositoryLeaseMatches' <<< "$leased_status")" == "true" ]] || fail 'status did not verify the repository lease'
502
+ [[ "$(jq -r '.runtime.originalBranchDrifted' <<< "$leased_status")" == "false" ]] || fail 'status reported false branch drift'
503
+ 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
+ fail 'a second task acquired the persistent repository lease'
505
+ fi
506
+ pass 'persistent repository lease rejects a concurrent automation task'
507
+ [[ "$(git -C "$fixture" worktree list --porcelain | awk '/^worktree / { count++ } END { print count + 0 }')" -eq 1 ]] || fail 'in-place preparation created an unexpected worktree'
508
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-004.json")" == "PENDING" ]] || fail 'prepared task did not become PENDING'
509
+ pass 'contract approval switches to a leased task branch while deferring the sealed plan and contract to the product commit'
510
+
511
+ run_task "$task_root" ./scripts/automation/claim-task.sh TASK-TEST-004 >/dev/null
512
+ printf '%s\n' 'class OrchestratedFlowTest { fun expectedBehavior() = Unit }' > "$task_root/mobile-client/src/test/java/dev/example/orchestratorfixture/OrchestratedFlowTest.kt"
513
+ run_task "$task_root" ./scripts/automation/record-red.sh TASK-TEST-004 'expected missing behavior' -- dev.example.orchestratorfixture.OrchestratedFlowTest >/dev/null
514
+ printf '%s\n' 'class OrchestratedFlow { fun value() = "integrated" }' > "$task_root/mobile-client/src/main/java/dev/example/orchestratorfixture/OrchestratedFlow.kt"
515
+ run_task "$task_root" env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/quality-gate.sh TASK-TEST-004 >/dev/null
516
+ run_task "$task_root" ./scripts/automation/begin-review.sh TASK-TEST-004 >/dev/null
517
+ 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
+ run_task "$task_root" ./scripts/automation/acceptance-report.sh TASK-TEST-004 >/dev/null
519
+ [[ "$(jq -r '.changedPaths | length' "$runtime_root/evidence/TASK-TEST-004/acceptance-report.json")" -eq 2 ]] || fail 'acceptance package omitted product paths'
520
+ [[ "$(jq -r '.evidence.qualityGate' "$runtime_root/evidence/TASK-TEST-004/acceptance-report.json")" == "PASSED" ]] || fail 'acceptance package omitted quality-gate status'
521
+ acceptance_card="$(run_fixture ./scripts/automation/show-acceptance-review.sh TASK-TEST-004)"
522
+ [[ "$acceptance_card" == *"人工验收提醒"* ]] || fail 'acceptance review did not actively identify the human gate'
523
+ [[ "$acceptance_card" == *"P0 · 真实行为是否满足合同"* ]] || fail 'acceptance review omitted behavioral focus'
524
+ [[ "$acceptance_card" == *"P0 · 旧行为与范围是否被误伤"* ]] || fail 'acceptance review omitted regression and scope focus'
525
+ [[ "$acceptance_card" == *"sealed diff SHA"* ]] || fail 'acceptance review omitted sealed binding'
526
+ [[ "$acceptance_card" == *"成功集成后自动删除;失败或阻塞时保留"* ]] || fail 'acceptance review omitted task branch cleanup policy'
527
+ pass 'automated evidence becomes one focused, SHA-verified human acceptance card'
528
+
529
+ printf '%s\n' 'class OrchestratedFlow { fun value() = "tampered after review" }' > "$task_root/mobile-client/src/main/java/dev/example/orchestratorfixture/OrchestratedFlow.kt"
530
+ if run_fixture ./scripts/automation/show-acceptance-review.sh TASK-TEST-004 >/dev/null 2>&1; then
531
+ fail 'acceptance review displayed a diff changed after sealing'
532
+ fi
533
+ printf '%s\n' 'class OrchestratedFlow { fun value() = "integrated" }' > "$task_root/mobile-client/src/main/java/dev/example/orchestratorfixture/OrchestratedFlow.kt"
534
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-004.json")" == "AWAITING_HUMAN" ]] || fail 'read-only acceptance display changed task state'
535
+ pass 'acceptance display rejects a changed diff and never advances state'
536
+
537
+ original_branch="$(jq -er '.originalBranch' "$runtime_root/evidence/TASK-TEST-004/origin.json")"
538
+ if run_fixture env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/accept-and-integrate.sh TASK-TEST-004 '拒绝' >/dev/null 2>&1; then
539
+ fail 'integrator accepted an invalid final confirmation'
540
+ fi
541
+ pass 'integrator requires final acceptance bound to the sealed diff'
542
+
543
+ run_fixture env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/accept-and-integrate.sh TASK-TEST-004 '验收通过,提交到原分支。' >/dev/null
544
+ combined_commit="$(jq -er '.productCommit' "$runtime_root/workspaces/TASK-TEST-004.json")"
545
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-004.json")" == "COMPLETED" ]] || fail 'final integration did not reach COMPLETED'
546
+ [[ "$(git -C "$fixture" symbolic-ref --short HEAD)" == "$original_branch" ]] || fail 'integrator changed the original branch identity'
547
+ [[ -f "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/OrchestratedFlow.kt" ]] || fail 'product change was not integrated into original branch'
548
+ [[ "$(git -C "$fixture" rev-list --count "$approved_baseline..$combined_commit")" -eq 1 ]] || fail 'final integration created more than one task commit'
549
+ for combined_path in \
550
+ docs/plans/TASK-TEST-004.md \
551
+ automation/tasks/TASK-TEST-004.json \
552
+ mobile-client/src/main/java/dev/example/orchestratorfixture/OrchestratedFlow.kt \
553
+ mobile-client/src/test/java/dev/example/orchestratorfixture/OrchestratedFlowTest.kt; do
554
+ git -C "$fixture" diff-tree --no-commit-id --name-only -r "$combined_commit" | \
555
+ awk -v expected="$combined_path" '$0 == expected { found = 1 } END { exit !found }' || \
556
+ fail "combined task commit omitted $combined_path"
557
+ done
558
+ [[ "$(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
+ [[ "$(jq -r '.pushed' "$runtime_root/evidence/TASK-TEST-004/integration.json")" == "false" ]] || fail 'integration evidence did not forbid push'
560
+ [[ "$(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'
561
+ [[ "$(jq -r '.taskBranchDeleted' "$runtime_root/evidence/TASK-TEST-004/integration.json")" == "true" ]] || fail 'integration evidence did not record task branch deletion'
562
+ [[ "$(jq -r '.taskBranchDeleted' "$runtime_root/workspaces/TASK-TEST-004.json")" == "true" ]] || fail 'workspace metadata did not record task branch deletion'
563
+ if git -C "$fixture" show-ref --verify --quiet refs/heads/automation/task-test-004; then
564
+ fail 'successful in-place integration left the local task branch behind'
565
+ fi
566
+ completed_status="$(run_fixture ./scripts/automation/status.sh TASK-TEST-004)"
567
+ [[ "$(jq -r '.runtime.taskBranchExists' <<< "$completed_status")" == "false" ]] || fail 'completed status did not report task branch deletion'
568
+ [[ ! -d "$runtime_root/locks/repository.workspace.lease" ]] || fail 'successful integration did not release the repository lease'
569
+ [[ "$(git -C "$fixture" worktree list --porcelain | awk '/^worktree / { count++ } END { print count + 0 }')" -eq 1 ]] || fail 'final integration created an unexpected candidate worktree'
570
+ pass 'one verified commit reaches the original branch and deletes the integrated local task branch without push'
571
+
572
+ printf '%s\n' '# Abort archival safety plan' > "$fixture/docs/plans/TASK-TEST-008.md"
573
+ jq \
574
+ '.id = "TASK-TEST-008" |
575
+ .title = "Archive an interrupted in-place task" |
576
+ .planPath = "docs/plans/TASK-TEST-008.md" |
577
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.AbortArchiveTest"}]' \
578
+ "$fixture/automation/tasks/TASK-TEST-001.json" \
579
+ > "$fixture/automation/tasks/TASK-TEST-008.json"
580
+ run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-008 '批准方案,生成计划和任务合同。' >/dev/null
581
+ run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/approve-and-run.sh TASK-TEST-008 '合同已复核,批准自动执行到人工验收阶段。' >/dev/null
582
+ printf '%s\n' 'class AbortArchive { fun value() = "preserved" }' > "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/AbortArchive.kt"
583
+ printf '%s\n' 'must not be archived automatically' > "$fixture/out-of-contract.txt"
584
+ if run_fixture ./scripts/automation/abort-task.sh TASK-TEST-008 '中止任务,封存修改并恢复原分支。' >/dev/null 2>&1; then
585
+ fail 'abort archived an out-of-contract path'
586
+ fi
587
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-008.json")" == "PENDING" ]] || fail 'rejected abort changed task state'
588
+ [[ -d "$runtime_root/locks/repository.workspace.lease" ]] || fail 'rejected abort released the repository lease'
589
+ rm "$fixture/out-of-contract.txt"
590
+ run_fixture ./scripts/automation/abort-task.sh TASK-TEST-008 '中止任务,封存修改并恢复原分支。' >/dev/null
591
+ abort_recovery_commit="$(jq -er '.recoveryCommit' "$runtime_root/evidence/TASK-TEST-008/abort.json")"
592
+ 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
+ git -C "$fixture" cat-file -e "$abort_recovery_commit:docs/plans/TASK-TEST-008.md" || fail 'abort recovery commit omitted the sealed plan'
594
+ git -C "$fixture" cat-file -e "$abort_recovery_commit:automation/tasks/TASK-TEST-008.json" || fail 'abort recovery commit omitted the sealed contract'
595
+ [[ ! -f "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/AbortArchive.kt" ]] || fail 'abort left archived code on the original branch'
596
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-008.json")" == "ABORTED" ]] || fail 'allowed abort did not reach ABORTED'
597
+ [[ ! -d "$runtime_root/locks/repository.workspace.lease" ]] || fail 'allowed abort did not release the repository lease'
598
+ pass 'abort refuses unrelated files and archives allowed uncommitted changes in a recovery commit'
599
+
600
+ planning_only_baseline="$(git -C "$fixture" rev-parse HEAD)"
601
+ printf '%s\n' '# Planning-only abort plan' > "$fixture/docs/plans/TASK-TEST-009.md"
602
+ jq \
603
+ '.id = "TASK-TEST-009" |
604
+ .title = "Abort before product editing" |
605
+ .planPath = "docs/plans/TASK-TEST-009.md" |
606
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.PlanningOnlyAbortTest"}]' \
607
+ "$fixture/automation/tasks/TASK-TEST-001.json" \
608
+ > "$fixture/automation/tasks/TASK-TEST-009.json"
609
+ run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-009 '批准方案,生成计划和任务合同。' >/dev/null
610
+ run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/approve-and-run.sh TASK-TEST-009 '合同已复核,批准自动执行到人工验收阶段。' >/dev/null
611
+ run_fixture ./scripts/automation/abort-task.sh TASK-TEST-009 '中止任务,封存修改并恢复原分支。' >/dev/null
612
+ [[ "$(jq -r '.recoveryCommit' "$runtime_root/evidence/TASK-TEST-009/abort.json")" == "null" ]] || fail 'planning-only abort created a recovery commit'
613
+ [[ "$(jq -r '.planningOnlyArchivedWithoutCommit' "$runtime_root/evidence/TASK-TEST-009/abort.json")" == "true" ]] || fail 'planning-only abort did not record its no-commit archival policy'
614
+ [[ "$(git -C "$fixture" rev-parse HEAD)" == "$planning_only_baseline" ]] || fail 'planning-only abort changed original branch history'
615
+ [[ ! -e "$fixture/docs/plans/TASK-TEST-009.md" && ! -e "$fixture/automation/tasks/TASK-TEST-009.json" ]] || fail 'planning-only abort left generated artifacts in the original worktree'
616
+ pass 'aborting before product edits archives planning artifacts without creating a planning-only commit'
617
+
618
+ isolated_config_tmp="$(mktemp "$fixture/automation/.config.XXXXXX")"
619
+ jq '.workspaceStrategy = "isolatedWorktree"' "$fixture/automation/config.json" > "$isolated_config_tmp"
620
+ mv "$isolated_config_tmp" "$fixture/automation/config.json"
621
+ (
622
+ cd "$fixture"
623
+ git add automation/config.json
624
+ git commit -qm 'Use isolated workspace fixture mode'
625
+ )
626
+ printf '%s\n' '# Optional isolated-workspace plan' > "$fixture/docs/plans/TASK-TEST-007.md"
627
+ jq \
628
+ '.id = "TASK-TEST-007" |
629
+ .title = "Keep optional isolated workspace support" |
630
+ .planPath = "docs/plans/TASK-TEST-007.md" |
631
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.IsolatedFlowTest"}]' \
632
+ "$fixture/automation/tasks/TASK-TEST-001.json" \
633
+ > "$fixture/automation/tasks/TASK-TEST-007.json"
634
+ run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-007 '批准方案,生成计划和任务合同。' >/dev/null
635
+ run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/approve-and-run.sh TASK-TEST-007 '合同已复核,批准自动执行到人工验收阶段。' >/dev/null
636
+ isolated_task_root="$(jq -er '.taskRoot' "$runtime_root/workspaces/TASK-TEST-007.json")"
637
+ [[ "$isolated_task_root" != "$fixture" && -d "$isolated_task_root" ]] || fail 'optional isolated strategy did not create a separate task root'
638
+ [[ -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
+ [[ ! -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
+ [[ "$(git -C "$fixture" worktree list --porcelain | awk '/^worktree / { count++ } END { print count + 0 }')" -eq 2 ]] || fail 'isolated strategy created more than one additional worktree'
641
+ run_task "$isolated_task_root" ./scripts/automation/claim-task.sh TASK-TEST-007 >/dev/null
642
+ printf '%s\n' 'class IsolatedFlowTest { fun expectedBehavior() = Unit }' > "$isolated_task_root/mobile-client/src/test/java/dev/example/orchestratorfixture/IsolatedFlowTest.kt"
643
+ run_task "$isolated_task_root" ./scripts/automation/record-red.sh TASK-TEST-007 'expected missing behavior' -- dev.example.orchestratorfixture.IsolatedFlowTest >/dev/null
644
+ printf '%s\n' 'class IsolatedFlow { fun value() = "integrated" }' > "$isolated_task_root/mobile-client/src/main/java/dev/example/orchestratorfixture/IsolatedFlow.kt"
645
+ run_task "$isolated_task_root" env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/quality-gate.sh TASK-TEST-007 >/dev/null
646
+ run_task "$isolated_task_root" ./scripts/automation/begin-review.sh TASK-TEST-007 >/dev/null
647
+ run_task "$isolated_task_root" env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/submit-review.sh TASK-TEST-007 APPROVED 'Independent review approves the isolated fixture.' >/dev/null
648
+ run_fixture env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/accept-and-integrate.sh TASK-TEST-007 '验收通过,提交到原分支。' >/dev/null
649
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-007.json")" == "COMPLETED" ]] || fail 'isolated task did not reach COMPLETED'
650
+ [[ "$(jq -r '.method' "$runtime_root/evidence/TASK-TEST-007/integration.json")" == "isolatedWorktree-fast-forward" ]] || fail 'isolated task used the wrong integration method'
651
+ [[ ! -d "$isolated_task_root" ]] || fail 'isolated task root was not cleaned after successful integration'
652
+ [[ "$(git -C "$fixture" worktree list --porcelain | awk '/^worktree / { count++ } END { print count + 0 }')" -eq 1 ]] || fail 'isolated integration left an extra candidate worktree'
653
+ if git -C "$fixture" show-ref --verify --quiet refs/heads/automation/task-test-007; then
654
+ fail 'successful isolated integration left the local task branch behind'
655
+ fi
656
+ [[ "$(jq -r '.taskBranchDeleted' "$runtime_root/evidence/TASK-TEST-007/integration.json")" == "true" ]] || fail 'isolated integration did not record task branch deletion'
657
+ [[ -f "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/IsolatedFlow.kt" ]] || fail 'isolated product change was not integrated'
658
+ pass 'optional isolation removes its task worktree and integrated local task branch without a second candidate'
659
+
660
+ in_place_config_tmp="$(mktemp "$fixture/automation/.config.XXXXXX")"
661
+ jq '.workspaceStrategy = "inPlaceExclusive"' "$fixture/automation/config.json" > "$in_place_config_tmp"
662
+ mv "$in_place_config_tmp" "$fixture/automation/config.json"
663
+ (
664
+ cd "$fixture"
665
+ git add automation/config.json
666
+ git commit -qm 'Restore in-place fixture mode'
667
+ )
668
+
669
+ printf '%s\n' '# Advanced-original integration plan' > "$fixture/docs/plans/TASK-TEST-006.md"
670
+ jq \
671
+ '.id = "TASK-TEST-006" |
672
+ .title = "Block an advanced original branch" |
673
+ .planPath = "docs/plans/TASK-TEST-006.md" |
674
+ .targetTests = [{gradleTask: "testDebugUnitTest", filter: "dev.example.orchestratorfixture.AdvancedOriginalTest"}]' \
675
+ "$fixture/automation/tasks/TASK-TEST-001.json" \
676
+ > "$fixture/automation/tasks/TASK-TEST-006.json"
677
+ run_fixture ./scripts/automation/prepare-contract-review.sh TASK-TEST-006 '批准方案,生成计划和任务合同。' >/dev/null
678
+ run_fixture env AUTOMATION_SKIP_AGENT_RUN=1 ./scripts/automation/approve-and-run.sh TASK-TEST-006 '合同已复核,批准自动执行到人工验收阶段。' >/dev/null
679
+ advanced_task_root="$(jq -er '.taskRoot' "$runtime_root/workspaces/TASK-TEST-006.json")"
680
+ run_task "$advanced_task_root" ./scripts/automation/claim-task.sh TASK-TEST-006 >/dev/null
681
+ printf '%s\n' 'class AdvancedOriginalTest { fun expectedBehavior() = Unit }' > "$advanced_task_root/mobile-client/src/test/java/dev/example/orchestratorfixture/AdvancedOriginalTest.kt"
682
+ run_task "$advanced_task_root" ./scripts/automation/record-red.sh TASK-TEST-006 'expected missing behavior' -- dev.example.orchestratorfixture.AdvancedOriginalTest >/dev/null
683
+ printf '%s\n' 'class AdvancedOriginal { fun value() = "must not integrate after drift" }' > "$advanced_task_root/mobile-client/src/main/java/dev/example/orchestratorfixture/AdvancedOriginal.kt"
684
+ run_task "$advanced_task_root" env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/quality-gate.sh TASK-TEST-006 >/dev/null
685
+ run_task "$advanced_task_root" ./scripts/automation/begin-review.sh TASK-TEST-006 >/dev/null
686
+ run_task "$advanced_task_root" env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/submit-review.sh TASK-TEST-006 APPROVED 'Independent review approves the advanced-branch fixture.' >/dev/null
687
+
688
+ advanced_baseline="$(jq -er '.baselineHead' "$runtime_root/workspaces/TASK-TEST-006.json")"
689
+ advanced_original_branch="$(jq -er '.originalBranch' "$runtime_root/workspaces/TASK-TEST-006.json")"
690
+ advanced_original_commit="$(printf '%s\n' 'Advance original branch independently' | git -C "$fixture" commit-tree "$advanced_baseline^{tree}" -p "$advanced_baseline")"
691
+ git -C "$fixture" update-ref "refs/heads/$advanced_original_branch" "$advanced_original_commit" "$advanced_baseline"
692
+ [[ "$(git -C "$fixture" rev-parse "refs/heads/$advanced_original_branch")" == "$advanced_original_commit" ]] || fail 'original branch did not advance for drift test'
693
+ pass 'a clean descendant commit is recognized as original-branch drift'
694
+
695
+ if run_fixture env AUTOMATION_FAKE_GREEN=1 ./scripts/automation/accept-and-integrate.sh TASK-TEST-006 '验收通过,提交到原分支。' >/dev/null 2>&1; then
696
+ fail 'integrator accepted an original branch that drifted from the recorded pre-task baseline'
697
+ fi
698
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-006.json")" == "INTEGRATION_BLOCKED" ]] || fail 'branch drift did not create INTEGRATION_BLOCKED'
699
+ [[ "$(git -C "$fixture" rev-parse "refs/heads/$advanced_original_branch")" == "$advanced_original_commit" ]] || fail 'blocked integration changed the advanced original branch'
700
+ git -C "$fixture" show-ref --verify --quiet refs/heads/automation/task-test-006 || fail 'blocked integration deleted the recoverable task branch'
701
+ [[ ! -f "$runtime_root/evidence/TASK-TEST-006/integration.json" ]] || fail 'blocked integration recorded false success evidence'
702
+ pass 'original-branch drift blocks integration while preserving the original and task branches'
703
+
704
+ if run_fixture ./scripts/automation/abort-task.sh TASK-TEST-006 '中止' >/dev/null 2>&1; then
705
+ fail 'abort accepted an unbound confirmation'
706
+ fi
707
+ run_fixture ./scripts/automation/abort-task.sh TASK-TEST-006 '中止任务,封存修改并恢复原分支。' >/dev/null
708
+ [[ "$(jq -r '.state' "$runtime_root/state/TASK-TEST-006.json")" == "ABORTED" ]] || fail 'deterministic abort did not reach ABORTED'
709
+ [[ "$(git -C "$fixture" symbolic-ref --short HEAD)" == "$advanced_original_branch" ]] || fail 'abort did not restore the source directory to the original branch'
710
+ [[ "$(git -C "$fixture" rev-parse HEAD)" == "$advanced_original_commit" ]] || fail 'abort changed the advanced original branch ref'
711
+ [[ "$(jq -r '.recoveryCommit' "$runtime_root/evidence/TASK-TEST-006/abort.json")" == "$(jq -r '.productCommit' "$runtime_root/workspaces/TASK-TEST-006.json")" ]] || fail 'abort did not preserve the product commit as recovery evidence'
712
+ [[ ! -f "$fixture/mobile-client/src/main/java/dev/example/orchestratorfixture/AdvancedOriginal.kt" ]] || fail 'aborted product code remained in the restored original working tree'
713
+ [[ ! -d "$runtime_root/locks/repository.workspace.lease" ]] || fail 'abort did not release the repository lease'
714
+ pass 'explicit abort preserves recovery evidence and restores the original branch without changing its ref'
715
+
716
+ printf '1..%d\n' "$pass_count"