@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,120 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ # shellcheck source=lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ task_id="${1:-}"
9
+ [[ "$#" -eq 1 ]] || { printf 'Usage: %s TASK-ID\n' "$0" >&2; exit 2; }
10
+ automation_validate_task_id "$task_id"
11
+ automation_require_orchestrated
12
+
13
+ workspace_file="$(automation_workspace_path "$task_id")"
14
+ evidence_dir="$(automation_evidence_path "$task_id")"
15
+ state_file="$(automation_state_path "$task_id")"
16
+ ready_file="$evidence_dir/ready.json"
17
+ transitions_file="$evidence_dir/transitions.jsonl"
18
+ resumptions_file="$evidence_dir/review-resumptions.jsonl"
19
+
20
+ [[ -f "$workspace_file" ]] || automation_die "workspace metadata is missing for $task_id"
21
+ [[ -f "$state_file" ]] || automation_die "state is missing for $task_id"
22
+ [[ -f "$evidence_dir/baseline.json" ]] || automation_die "baseline evidence is missing"
23
+ [[ -f "$evidence_dir/red.json" ]] || automation_die "RED evidence is missing"
24
+ [[ -f "$ready_file" ]] || automation_die "ready evidence is missing"
25
+ [[ -f "$transitions_file" ]] || automation_die "transition evidence is missing"
26
+
27
+ automation_acquire_run_lock "$task_id"
28
+ trap 'automation_release_run_lock' EXIT
29
+
30
+ [[ "$(automation_read_state "$task_id")" == "BLOCKED" ]] || automation_die "$task_id is not BLOCKED"
31
+
32
+ task_root="$(automation_workspace_task_root "$workspace_file")"
33
+ workspace_strategy="$(automation_workspace_strategy "$workspace_file")"
34
+ source_root="$(jq -er '.sourceRoot' "$workspace_file")"
35
+ source_root="$(jq -er '.sourceRoot' "$workspace_file")"
36
+ task_branch="$(jq -er '.taskBranch' "$workspace_file")"
37
+ baseline_head="$(jq -er '.baselineHead' "$workspace_file")"
38
+ coding_cycle="$(jq -er '.codingCycle // 0' "$workspace_file")"
39
+
40
+ [[ -d "$task_root" ]] || automation_die "task root is missing: $task_root"
41
+ if [[ "$(jq -r '.repositoryLeaseRequired // false' "$workspace_file")" == "true" ]]; then
42
+ automation_require_repository_lease "$task_id" "$source_root" "$workspace_strategy"
43
+ fi
44
+ task_root="$(cd "$task_root" && pwd -P)"
45
+ source_root="$(cd "$source_root" && pwd -P)"
46
+ [[ "$source_root" == "$AUTOMATION_ROOT" ]] || automation_die "workspace sourceRoot does not match this repository"
47
+ [[ "$(automation_current_branch "$task_root")" == "$task_branch" ]] || automation_die "task branch changed"
48
+ [[ "$(git -C "$task_root" rev-parse HEAD)" == "$baseline_head" ]] || automation_die "task HEAD changed after sealing"
49
+ [[ "$(jq -er '.head' "$evidence_dir/baseline.json")" == "$baseline_head" ]] || automation_die "baseline evidence does not match workspace metadata"
50
+ [[ "$(jq -er '.head' "$ready_file")" == "$baseline_head" ]] || automation_die "ready evidence does not match the baseline"
51
+ [[ "$(jq -er '.codingCycle // 0' "$ready_file")" == "$coding_cycle" ]] || automation_die "ready evidence belongs to a different coding cycle"
52
+
53
+ if ! jq -es -e '
54
+ ([to_entries[] |
55
+ select(.value.from == "REVIEWING" and
56
+ .value.to == "BLOCKED" and
57
+ .value.actor == "orchestrator" and
58
+ (.value.note | startswith("reviewer exited without submitting a decision")))] | last) as $failure
59
+ | ($failure != null) and
60
+ all(.[$failure.key + 1:][];
61
+ (.from == "BLOCKED" and .to == "PENDING" and .actor == "human") or
62
+ (.from == "PENDING" and .to == "BLOCKED" and
63
+ (.actor == "coder" or .actor == "preflight" or .actor == "orchestrator")))
64
+ ' "$transitions_file" >/dev/null; then
65
+ automation_die "BLOCKED state is not a recoverable reviewer interruption"
66
+ fi
67
+
68
+ ready_diff_sha="$(jq -er '.diffSha256' "$ready_file")"
69
+ current_diff_sha="$(automation_worktree_diff_sha "$task_root")"
70
+ [[ "$current_diff_sha" == "$ready_diff_sha" ]] || automation_die "sealed diff changed; reviewer-only recovery is forbidden"
71
+
72
+ if [[ -f "$evidence_dir/reviews.jsonl" ]] &&
73
+ jq -es -e --arg sha "$ready_diff_sha" 'any(.[]; .diffSha256 == $sha)' "$evidence_dir/reviews.jsonl" >/dev/null; then
74
+ automation_die "a reviewer decision already exists for the sealed diff"
75
+ fi
76
+ if [[ -f "$evidence_dir/review.json" ]] &&
77
+ jq -e --arg sha "$ready_diff_sha" '.diffSha256 == $sha' "$evidence_dir/review.json" >/dev/null; then
78
+ automation_die "a reviewer decision already exists for the sealed diff"
79
+ fi
80
+
81
+ restart_count=0
82
+ if [[ -f "$resumptions_file" ]]; then
83
+ restart_count="$(jq -s --arg sha "$ready_diff_sha" '[.[] | select(.diffSha256 == $sha)] | length' "$resumptions_file")"
84
+ fi
85
+ max_restarts="$(automation_config_value '.maxReviewerRestarts')"
86
+ [[ "$restart_count" -lt "$max_restarts" ]] || automation_die "reviewer restart limit exhausted for the sealed diff"
87
+ next_restart=$((restart_count + 1))
88
+
89
+ (
90
+ cd "$task_root"
91
+ ./scripts/automation/scope-gate.sh "$task_id" >/dev/null
92
+ )
93
+
94
+ blocked_revision="$(jq -er '.revision' "$state_file")"
95
+ automation_transition_state \
96
+ "$task_id" \
97
+ "BLOCKED" \
98
+ "REVIEWING" \
99
+ "human" \
100
+ "reviewer-only recovery $next_restart/$max_restarts; sealed diff preserved and coder bypassed"
101
+
102
+ jq -nc \
103
+ --arg taskId "$task_id" \
104
+ --arg at "$(automation_now)" \
105
+ --arg diffSha256 "$ready_diff_sha" \
106
+ --argjson blockedRevision "$blocked_revision" \
107
+ --argjson restart "$next_restart" \
108
+ --argjson maxRestarts "$max_restarts" \
109
+ --argjson codingCycle "$coding_cycle" \
110
+ '{taskId: $taskId, at: $at, diffSha256: $diffSha256,
111
+ blockedRevision: $blockedRevision, restart: $restart,
112
+ maxRestarts: $maxRestarts, codingCycle: $codingCycle,
113
+ route: "BLOCKED->REVIEWING", coderRerun: false}' \
114
+ | automation_append_json "$resumptions_file"
115
+
116
+ automation_info "$task_id reviewer-only recovery accepted; sealed diff $ready_diff_sha"
117
+ automation_release_run_lock
118
+ trap - EXIT
119
+
120
+ "$SCRIPT_DIR/orchestrate-task.sh" "$task_id"
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ # shellcheck source=lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ task_id="${1:-}"
9
+ [[ -n "$task_id" ]] || { printf 'Usage: %s TASK-ID\n' "$0" >&2; exit 2; }
10
+ automation_validate_task_id "$task_id"
11
+ "$SCRIPT_DIR/validate-contract.sh" "$task_id" >/dev/null
12
+
13
+ contract="$(automation_contract_path "$task_id")"
14
+ evidence_dir="$(automation_evidence_path "$task_id")"
15
+ baseline_meta="$evidence_dir/baseline.json"
16
+ [[ -f "$baseline_meta" ]] || { automation_die "baseline evidence is missing"; exit 40; }
17
+
18
+ baseline_head="$(jq -er '.head' "$baseline_meta")"
19
+ current_head="$(git -C "$AUTOMATION_ROOT" rev-parse HEAD)"
20
+ [[ "$baseline_head" == "$current_head" ]] || { automation_die "HEAD changed after baseline"; exit 40; }
21
+
22
+ workspace_file="$(automation_workspace_path "$task_id")"
23
+ planning_commit_policy="$(jq -r '.planningArtifactsCommitPolicy // "legacyCommittedSeparately"' "$workspace_file")"
24
+ if [[ "$planning_commit_policy" == "withProductChanges" ]]; then
25
+ automation_assert_planning_artifacts_sealed "$task_id" "$AUTOMATION_ROOT" || exit 40
26
+ changed_file_list="$(automation_product_changed_paths_at "$task_id" "$AUTOMATION_ROOT")"
27
+ [[ -n "$changed_file_list" ]] || { automation_die "task has no product changes beyond its sealed planning artifacts"; exit 40; }
28
+ else
29
+ changed_file_list="$(automation_changed_paths)"
30
+ [[ -n "$changed_file_list" ]] || { automation_die "task has no changed files"; exit 40; }
31
+ fi
32
+
33
+ max_changed="$(jq -r '.maxChangedFiles' "$contract")"
34
+ changed_count="$(printf '%s\n' "$changed_file_list" | awk 'NF { count++ } END { print count + 0 }')"
35
+ [[ "$changed_count" -le "$max_changed" ]] || { automation_die "changed file count $changed_count exceeds limit $max_changed"; exit 40; }
36
+
37
+ production_changed=0
38
+ test_changed=0
39
+ while IFS= read -r path; do
40
+ [[ -n "$path" ]] || continue
41
+
42
+ while IFS= read -r protected; do
43
+ if automation_path_matches "$path" "$protected"; then
44
+ automation_die "protected path changed: $path"
45
+ exit 40
46
+ fi
47
+ done < <(jq -r '.protectedPaths[]' "$AUTOMATION_CONFIG")
48
+
49
+ if automation_array_matches_path "$contract" '.forbiddenPaths' "$path"; then
50
+ automation_die "forbidden path changed: $path"
51
+ exit 40
52
+ fi
53
+ if ! automation_array_matches_path "$contract" '.allowedPaths' "$path"; then
54
+ automation_die "path is outside allowedPaths: $path"
55
+ exit 40
56
+ fi
57
+
58
+ if automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.productionPaths' "$path"; then
59
+ production_changed=1
60
+ fi
61
+ if automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$path"; then
62
+ test_changed=1
63
+ fi
64
+ done <<< "$changed_file_list"
65
+
66
+ deleted_tests="$(
67
+ git -C "$AUTOMATION_ROOT" diff --name-only --diff-filter=D HEAD -- \
68
+ | while IFS= read -r path; do
69
+ if automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$path"; then
70
+ printf '%s\n' "$path"
71
+ fi
72
+ done
73
+ )"
74
+ [[ -z "$deleted_tests" ]] || { automation_die "test deletion is forbidden: $deleted_tests"; exit 40; }
75
+
76
+ test_policy="$(jq -r '.testPolicy' "$contract")"
77
+ if [[ "$production_changed" == "1" && "$test_policy" == "required" && "$test_changed" != "1" ]]; then
78
+ automation_die "production code changed without a test change"
79
+ exit 40
80
+ fi
81
+
82
+ added_test_lines="$(
83
+ while IFS= read -r path; do
84
+ [[ -n "$path" ]] || continue
85
+ if automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$path"; then
86
+ git -C "$AUTOMATION_ROOT" diff --unified=0 HEAD -- "$path"
87
+ fi
88
+ done <<< "$changed_file_list" \
89
+ | awk '/^\+\+\+/ { next } /^\+/ { sub(/^\+/, ""); print }'
90
+ )"
91
+ weakening_pattern='@Ignore|@Disabled|assumeTrue\(false\)|assertTrue\(true\)|assertFalse\(false\)'
92
+ if printf '%s\n' "$added_test_lines" | rg -n "$weakening_pattern" >/dev/null; then
93
+ automation_die "potential test weakening detected in added lines"
94
+ exit 40
95
+ fi
96
+
97
+ while IFS= read -r untracked_test; do
98
+ if automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$untracked_test" && \
99
+ rg -n "$weakening_pattern" "$AUTOMATION_ROOT/$untracked_test" >/dev/null; then
100
+ automation_die "potential test weakening detected in new test: $untracked_test"
101
+ exit 40
102
+ fi
103
+ done < <(git -C "$AUTOMATION_ROOT" ls-files --others --exclude-standard)
104
+
105
+ if [[ "$planning_commit_policy" == "withProductChanges" ]]; then
106
+ automation_info "scope gate passed ($changed_count product files; sealed planning artifacts excluded from the contract limit)"
107
+ else
108
+ automation_info "scope gate passed ($changed_count changed files)"
109
+ fi
110
+ printf '%s\n' "$changed_file_list"
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ # shellcheck source=lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ wanted="${1:-}"
9
+ case "$wanted" in
10
+ PENDING|READY_FOR_REVIEW|REVIEWING) ;;
11
+ *) printf 'Usage: %s PENDING|READY_FOR_REVIEW|REVIEWING\n' "$0" >&2; exit 2 ;;
12
+ esac
13
+
14
+ matches=()
15
+ while IFS= read -r state_file; do
16
+ [[ -f "$state_file" ]] || continue
17
+ state="$(jq -er '.state' "$state_file" 2>/dev/null || true)"
18
+ task_id="$(jq -er '.taskId' "$state_file" 2>/dev/null || true)"
19
+ if [[ "$state" == "$wanted" && "$task_id" =~ ^TASK-[A-Z0-9-]+$ ]]; then
20
+ matches+=("$task_id")
21
+ fi
22
+ done < <(find "$AUTOMATION_STATE_DIR" -maxdepth 1 -type f -name 'TASK-*.json' | LC_ALL=C sort)
23
+
24
+ if [[ "${#matches[@]}" -eq 0 ]]; then
25
+ automation_info "no task is in state $wanted"
26
+ exit 3
27
+ fi
28
+ if [[ "${#matches[@]}" -ne 1 ]]; then
29
+ automation_die "expected one $wanted task, found ${#matches[@]}: ${matches[*]}"
30
+ exit 4
31
+ fi
32
+
33
+ "$SCRIPT_DIR/validate-contract.sh" "${matches[0]}" >/dev/null
34
+ printf '%s\n' "${matches[0]}"
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ # shellcheck source=lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ automation_info "starting read-only shadow preflight" >&2
9
+ "$SCRIPT_DIR/preflight.sh" --shadow >&2
10
+
11
+ task_count="$(find "$AUTOMATION_TASKS_DIR" -maxdepth 1 -type f -name 'TASK-*.json' | wc -l | tr -d ' ')"
12
+ automation_ensure_runtime_layout
13
+ state_count="$(find "$AUTOMATION_STATE_DIR" -maxdepth 1 -type f -name 'TASK-*.json' | wc -l | tr -d ' ')"
14
+
15
+ jq -n \
16
+ --arg checkedAt "$(automation_now)" \
17
+ --arg root "$AUTOMATION_ROOT" \
18
+ --arg enabled "$(automation_config_value '.enabled')" \
19
+ --arg mode "$(automation_config_value '.mode')" \
20
+ --argjson taskCount "$task_count" \
21
+ --argjson stateCount "$state_count" \
22
+ '{checkedAt: $checkedAt, root: $root, enabled: ($enabled == "true"), mode: $mode, activeContracts: $taskCount, runtimeStates: $stateCount, mutationPerformed: false}'
23
+
24
+ automation_info "shadow run complete; no repository mutation performed" >&2
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ # shellcheck source=lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ task_id="${1:-}"
9
+ [[ "$#" -eq 1 ]] || { printf 'Usage: %s TASK-ID\n' "$0" >&2; exit 2; }
10
+ automation_validate_task_id "$task_id"
11
+ [[ "$(automation_read_state "$task_id")" == "AWAITING_HUMAN" ]] || automation_die "$task_id is not AWAITING_HUMAN"
12
+
13
+ workspace_file="$(automation_workspace_path "$task_id")"
14
+ evidence_dir="$(automation_evidence_path "$task_id")"
15
+ ready_file="$evidence_dir/ready.json"
16
+ review_file="$evidence_dir/review.json"
17
+ report_file="$evidence_dir/acceptance-report.json"
18
+ [[ -f "$workspace_file" && -f "$ready_file" && -f "$review_file" ]] || \
19
+ automation_die "acceptance evidence is incomplete"
20
+
21
+ task_root="$(automation_workspace_task_root "$workspace_file")"
22
+ [[ -d "$task_root" ]] || automation_die "recorded task root is missing: $task_root"
23
+ source_root="$(jq -er '.sourceRoot' "$workspace_file")"
24
+ original_branch="$(jq -er '.originalBranch' "$workspace_file")"
25
+ live_original_head="$(git -C "$source_root" rev-parse "refs/heads/$original_branch")"
26
+
27
+ if [[ ! -f "$report_file" ]] || \
28
+ [[ "$(jq -r '.originalHeadCurrent // ""' "$report_file" 2>/dev/null || true)" != "$live_original_head" ]]; then
29
+ (
30
+ cd "$task_root"
31
+ ./scripts/automation/acceptance-report.sh "$task_id" >/dev/null
32
+ )
33
+ fi
34
+
35
+ [[ "$(jq -er '.taskId' "$report_file")" == "$task_id" ]] || automation_die "acceptance report task ID does not match"
36
+ [[ "$(jq -er '.state' "$report_file")" == "AWAITING_HUMAN" ]] || automation_die "acceptance report is not awaiting human review"
37
+ [[ "$(jq -er '.decision' "$review_file")" == "APPROVED" ]] || automation_die "latest independent review is not approved"
38
+
39
+ current_diff_sha="$(automation_worktree_diff_sha "$task_root")"
40
+ report_diff_sha="$(jq -er '.sealedDiffSha256' "$report_file")"
41
+ [[ "$current_diff_sha" == "$report_diff_sha" ]] || automation_die "sealed diff changed after the acceptance package was generated"
42
+ [[ "$current_diff_sha" == "$(jq -er '.diffSha256' "$ready_file")" ]] || automation_die "sealed diff no longer matches the quality gate"
43
+ [[ "$current_diff_sha" == "$(jq -er '.diffSha256' "$review_file")" ]] || automation_die "sealed diff no longer matches independent review"
44
+
45
+ changed_count="$(jq -er '.changedPaths | length' "$report_file")"
46
+ max_changed_files="$(jq -er '.maxChangedFiles' "$report_file")"
47
+ device_tests_required="$(jq -r '.deviceTestsRequired' "$report_file")"
48
+
49
+ printf '# 🔔 人工验收提醒\n\n'
50
+ printf '自动执行已停在 `AWAITING_HUMAN`。下面内容已重新核对 sealed diff;此时代码、计划与任务合同均尚未提交或集成。\n\n'
51
+ printf '| 绑定项 | 当前封存值 |\n'
52
+ printf '| --- | --- |\n'
53
+ printf '| 任务 | `%s` · %s |\n' "$task_id" "$(jq -r '.title' "$report_file")"
54
+ printf '| 原分支 | `%s` |\n' "$(jq -r '.originalBranch' "$report_file")"
55
+ printf '| 原分支当前 HEAD | `%s` |\n' "$(jq -r '.originalHeadCurrent' "$report_file")"
56
+ printf '| 原分支漂移 | `%s` |\n' "$(jq -r 'if .originalBranchDrifted then "是(集成将阻断)" else "否" end' "$report_file")"
57
+ printf '| 任务分支收尾 | 成功集成后自动删除;失败或阻塞时保留 |\n'
58
+ printf '| sealed diff SHA | `%s` |\n' "$report_diff_sha"
59
+ printf '| 变更范围 | 实际 %s 个 / 合同上限 %s 个 |\n' "$changed_count" "$max_changed_files"
60
+ printf '| 提交策略 | 代码、测试、计划与任务合同合并为一个提交 |\n'
61
+ printf '| 自动证据 | Baseline ✓ · RED ✓ · G1–G6 ✓ · Reviewer APPROVED ✓ |\n'
62
+ printf '\n## 必须重点复核\n\n'
63
+ printf '### P0 · 真实行为是否满足合同\n\n'
64
+ printf '这是人工验收的核心;请按以下条件实际操作或检查结果,不要只看测试为绿:\n\n'
65
+ jq -r '.acceptanceCriteria | to_entries[] | "\(.key + 1). \(.value)"' "$report_file"
66
+
67
+ printf '\n### P0 · 旧行为与范围是否被误伤\n\n'
68
+ printf '**实际变更文件:**\n\n'
69
+ jq -r '.changedPaths[] | "- `\(.)`"' "$report_file"
70
+ printf '\n**随产品变更一并提交的封存规划文件(不计入合同文件上限):**\n\n'
71
+ jq -r '.planningArtifacts[] | "- `\(.)`"' "$report_file"
72
+ printf '\n**合同允许路径:**\n\n'
73
+ jq -r '.allowedPaths[] | "- `\(.)`"' "$report_file"
74
+ printf '\n**明确不应发生:**\n\n'
75
+ jq -r '.nonGoals[] | "- \(.)"' "$report_file"
76
+
77
+ printf '\n### P1 · 自动证据是否可信\n\n'
78
+ printf -- '- RED 退出码:`%s`(应为非 0,证明测试先真实失败)\n' "$(jq -r '.evidence.redExitCode' "$report_file")"
79
+ printf -- '- 质量门:`%s`,第 %s 个编码周期内共运行 %s 次\n' \
80
+ "$(jq -r '.evidence.qualityGate' "$report_file")" \
81
+ "$(jq -r '.evidence.codingCycle' "$report_file")" \
82
+ "$(jq -r '.evidence.gateAttempts' "$report_file")"
83
+ printf -- '- Reviewer:`%s`,独立复验退出码 `%s`\n' \
84
+ "$(jq -r '.evidence.reviewerDecision' "$report_file")" \
85
+ "$(jq -r '.evidence.reviewerVerificationExitCode' "$report_file")"
86
+ printf -- '- Reviewer 摘要:%s\n' "$(jq -r '.reviewSummary' "$report_file")"
87
+ printf -- '- 聚焦测试:\n'
88
+ jq -r '.targetTests[] | " - `./gradlew \(.gradleTask) --tests \(.filter)`"' "$report_file"
89
+
90
+ printf '\n### P1 · 绑定与剩余风险\n\n'
91
+ printf -- '- 当前 diff 与质量门、独立 Review、验收包的 SHA 三方一致。\n'
92
+ printf -- '- 目标接收分支是 `%s`;通过后只做本地集成,`pushed: false`。\n' "$(jq -r '.originalBranch' "$report_file")"
93
+ printf -- '- 原分支安全到达已验证提交后,本地任务分支 `%s` 将被自动删除;集成失败时保留以便恢复。\n' \
94
+ "$(jq -r '.taskBranch' "$report_file")"
95
+ if [[ "$(jq -r '.originalBranchDrifted' "$report_file")" == "true" ]]; then
96
+ printf -- '- **原分支已偏离批准基线;当前策略会进入 `INTEGRATION_BLOCKED`,不会自动 cherry-pick。**\n'
97
+ fi
98
+ if [[ "$device_tests_required" == "true" ]]; then
99
+ printf -- '- 合同要求设备测试,自动质量门已执行;仍应人工确认真机/模拟器上的可观察体验。\n'
100
+ else
101
+ printf -- '- 合同未要求设备测试。如果变更涉及 UI、系统权限或真机差异,请在通过前补做人工体验。\n'
102
+ fi
103
+ printf -- '- 测试策略:`%s`。%s\n' \
104
+ "$(jq -r '.testPolicy' "$report_file")" \
105
+ "$(jq -r '.testPolicyReason' "$report_file")"
106
+
107
+ printf '\n## 你的决定\n\n'
108
+ printf -- '- 全部重点通过:选择 **验收通过,提交到原分支。**\n'
109
+ printf -- '- 任一项不通过:选择 **验收不通过,需要说明失败项**,并指出失败的验收条件或观察结果。\n'
110
+ printf -- '- 还未检查完:选择 **暂不决定,保持封存**;系统继续停在 `AWAITING_HUMAN`。\n'
111
+ printf '\n> 不要修改已封存的任务目录后沿用本次验收;diff 一旦变化,脚本会拒绝旧验收。\n'
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ # shellcheck source=lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ task_id="${1:-}"
9
+ [[ -n "$task_id" ]] || { printf 'Usage: %s TASK-ID\n' "$0" >&2; exit 2; }
10
+ automation_validate_task_id "$task_id"
11
+ automation_require_layout
12
+
13
+ contract="$(automation_contract_path "$task_id")"
14
+ state_file="$(automation_state_path "$task_id")"
15
+ workspace_file="$(automation_workspace_path "$task_id")"
16
+ origin_file="$(automation_origin_path "$task_id")"
17
+ evidence_dir="$(automation_evidence_path "$task_id")"
18
+
19
+ state_json="$(if [[ -f "$state_file" ]]; then jq -c . "$state_file"; else printf 'null'; fi)"
20
+ workspace_json="$(if [[ -f "$workspace_file" ]]; then jq -c . "$workspace_file"; else printf 'null'; fi)"
21
+ origin_json="$(if [[ -f "$origin_file" ]]; then jq -c . "$origin_file"; else printf 'null'; fi)"
22
+ baseline_json="$(if [[ -f "$evidence_dir/baseline.json" ]]; then jq -c . "$evidence_dir/baseline.json"; else printf 'null'; fi)"
23
+ red_json="$(if [[ -f "$evidence_dir/red.json" ]]; then jq -c . "$evidence_dir/red.json"; else printf 'null'; fi)"
24
+ ready_json="$(if [[ -f "$evidence_dir/ready.json" ]]; then jq -c . "$evidence_dir/ready.json"; else printf 'null'; fi)"
25
+ review_json="$(if [[ -f "$evidence_dir/review.json" ]]; then jq -c . "$evidence_dir/review.json"; else printf 'null'; fi)"
26
+ gate_json=null
27
+ if [[ "$ready_json" != "null" ]]; then
28
+ coding_cycle="$(jq -r '.codingCycle // 0' <<< "$ready_json")"
29
+ gate_file="$evidence_dir/gate-attempts-cycle-$coding_cycle.json"
30
+ if [[ -f "$gate_file" ]]; then
31
+ gate_json="$(jq -c . "$gate_file")"
32
+ fi
33
+ fi
34
+
35
+ current_diff_sha=""
36
+ lease_json=null
37
+ lease_matches=null
38
+ original_head_current=""
39
+ original_branch_drifted=null
40
+ task_branch_exists=null
41
+ if [[ "$workspace_json" != "null" ]]; then
42
+ task_root="$(jq -r '.taskRoot // .taskWorktree // empty' <<< "$workspace_json")"
43
+ if [[ -n "$task_root" && -d "$task_root" ]]; then
44
+ current_diff_sha="$(automation_worktree_diff_sha "$task_root")"
45
+ fi
46
+ source_root="$(jq -r '.sourceRoot // empty' <<< "$workspace_json")"
47
+ original_branch="$(jq -r '.originalBranch // empty' <<< "$workspace_json")"
48
+ task_branch="$(jq -r '.taskBranch // empty' <<< "$workspace_json")"
49
+ baseline_head="$(jq -r '.baselineHead // empty' <<< "$workspace_json")"
50
+ if [[ -n "$source_root" && -n "$task_branch" ]]; then
51
+ if git -C "$source_root" show-ref --verify --quiet "refs/heads/$task_branch"; then
52
+ task_branch_exists=true
53
+ else
54
+ task_branch_exists=false
55
+ fi
56
+ fi
57
+ if [[ -n "$source_root" && -n "$original_branch" ]] && \
58
+ git -C "$source_root" show-ref --verify --quiet "refs/heads/$original_branch"; then
59
+ original_head_current="$(git -C "$source_root" rev-parse "refs/heads/$original_branch")"
60
+ if [[ -n "$baseline_head" && "$original_head_current" != "$baseline_head" ]]; then
61
+ original_branch_drifted=true
62
+ else
63
+ original_branch_drifted=false
64
+ fi
65
+ fi
66
+ if [[ "$(jq -r '.repositoryLeaseRequired // false' <<< "$workspace_json")" == "true" ]]; then
67
+ lease_file="$(automation_repository_lease_dir)/lease.json"
68
+ if [[ -f "$lease_file" ]]; then
69
+ lease_json="$(jq -c . "$lease_file")"
70
+ if jq -e \
71
+ --arg taskId "$task_id" \
72
+ --arg sourceRoot "$source_root" \
73
+ --arg strategy "$(jq -r '.workspaceStrategy' <<< "$workspace_json")" \
74
+ '.taskId == $taskId and .sourceRoot == $sourceRoot and .workspaceStrategy == $strategy' \
75
+ "$lease_file" >/dev/null; then
76
+ lease_matches=true
77
+ else
78
+ lease_matches=false
79
+ fi
80
+ else
81
+ lease_matches=false
82
+ fi
83
+ fi
84
+ fi
85
+
86
+ [[ -f "$contract" ]] || automation_die "contract not found: $contract"
87
+ jq -n \
88
+ --slurpfile contract "$contract" \
89
+ --argjson state "$state_json" \
90
+ --argjson workspace "$workspace_json" \
91
+ --argjson origin "$origin_json" \
92
+ --argjson baseline "$baseline_json" \
93
+ --argjson red "$red_json" \
94
+ --argjson ready "$ready_json" \
95
+ --argjson gate "$gate_json" \
96
+ --argjson review "$review_json" \
97
+ --argjson repositoryLease "$lease_json" \
98
+ --argjson repositoryLeaseMatches "$lease_matches" \
99
+ --argjson taskBranchExists "$task_branch_exists" \
100
+ --arg originalHeadCurrent "$original_head_current" \
101
+ --argjson originalBranchDrifted "$original_branch_drifted" \
102
+ --arg evidence "$evidence_dir" \
103
+ --arg currentDiffSha256 "$current_diff_sha" \
104
+ '{contract: $contract[0], state: $state, workspace: $workspace, origin: $origin,
105
+ runtime: {
106
+ repositoryLease: $repositoryLease,
107
+ repositoryLeaseMatches: $repositoryLeaseMatches,
108
+ taskBranchExists: $taskBranchExists,
109
+ originalHeadCurrent: ($originalHeadCurrent | if length == 0 then null else . end),
110
+ originalBranchDrifted: $originalBranchDrifted
111
+ },
112
+ evidenceDirectory: $evidence,
113
+ evidence: {
114
+ directory: $evidence,
115
+ baseline: $baseline,
116
+ red: $red,
117
+ ready: $ready,
118
+ latestGate: $gate,
119
+ review: $review,
120
+ currentDiffSha256: ($currentDiffSha256 | if length == 0 then null else . end),
121
+ sealedDiffMatches: (if ($ready == null or ($currentDiffSha256 | length) == 0)
122
+ then null
123
+ else $ready.diffSha256 == $currentDiffSha256
124
+ end)
125
+ }}'
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
5
+ # shellcheck source=lib.sh
6
+ source "$SCRIPT_DIR/lib.sh"
7
+
8
+ task_id="${1:-}"
9
+ decision="${2:-}"
10
+ shift $(( $# >= 2 ? 2 : $# ))
11
+ summary="$*"
12
+
13
+ if [[ -z "$task_id" || -z "$decision" || -z "$summary" ]]; then
14
+ printf 'Usage: %s TASK-ID APPROVED|CHANGES_REQUESTED SUMMARY\n' "$0" >&2
15
+ exit 2
16
+ fi
17
+
18
+ automation_validate_task_id "$task_id"
19
+ [[ "$decision" == "APPROVED" || "$decision" == "CHANGES_REQUESTED" ]] || automation_die "invalid review decision: $decision"
20
+ [[ ${#summary} -ge 20 ]] || automation_die "review summary must contain at least 20 characters"
21
+ [[ "$(automation_read_state "$task_id")" == "REVIEWING" ]] || automation_die "$task_id is not REVIEWING"
22
+
23
+ evidence_dir="$(automation_evidence_path "$task_id")"
24
+ ready_meta="$evidence_dir/ready.json"
25
+ [[ -f "$ready_meta" ]] || automation_die "ready evidence is missing"
26
+ current_diff_sha="$(automation_worktree_diff_sha)"
27
+ ready_diff_sha="$(jq -er '.diffSha256' "$ready_meta")"
28
+
29
+ if [[ "$current_diff_sha" != "$ready_diff_sha" ]]; then
30
+ decision="CHANGES_REQUESTED"
31
+ summary="Diff changed after coder gate. $summary"
32
+ fi
33
+
34
+ review_log="$evidence_dir/review-verification.log"
35
+ verification_status=0
36
+ if [[ "$decision" == "APPROVED" ]]; then
37
+ set +e
38
+ "$SCRIPT_DIR/verify-task.sh" "$task_id" 2>&1 | tee "$review_log"
39
+ verification_status=${PIPESTATUS[0]}
40
+ set -e
41
+ if [[ "$verification_status" -ne 0 ]]; then
42
+ decision="CHANGES_REQUESTED"
43
+ summary="Independent verification failed with exit $verification_status. $summary"
44
+ fi
45
+ fi
46
+
47
+ jq -n \
48
+ --arg taskId "$task_id" \
49
+ --arg decision "$decision" \
50
+ --arg summary "$summary" \
51
+ --arg reviewedAt "$(automation_now)" \
52
+ --arg diffSha256 "$current_diff_sha" \
53
+ --argjson verificationExitCode "$verification_status" \
54
+ '{taskId: $taskId, decision: $decision, summary: $summary, reviewedAt: $reviewedAt, diffSha256: $diffSha256, verificationExitCode: $verificationExitCode}' \
55
+ | automation_record_json "$evidence_dir/review.json"
56
+ jq -nc \
57
+ --arg taskId "$task_id" \
58
+ --arg decision "$decision" \
59
+ --arg summary "$summary" \
60
+ --arg reviewedAt "$(automation_now)" \
61
+ --arg diffSha256 "$current_diff_sha" \
62
+ --argjson verificationExitCode "$verification_status" \
63
+ '{taskId: $taskId, decision: $decision, summary: $summary, reviewedAt: $reviewedAt, diffSha256: $diffSha256, verificationExitCode: $verificationExitCode}' \
64
+ | automation_append_json "$evidence_dir/reviews.jsonl"
65
+
66
+ if [[ "$decision" == "APPROVED" ]]; then
67
+ automation_transition_state "$task_id" "REVIEWING" "AWAITING_HUMAN" "reviewer" "independent review approved; human acceptance required"
68
+ automation_info "$task_id approved and awaiting human acceptance"
69
+ else
70
+ automation_transition_state "$task_id" "REVIEWING" "CHANGES_REQUESTED" "reviewer" "$summary"
71
+ automation_warn "$task_id requires changes"
72
+ exit 1
73
+ fi