@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,201 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:frankzhang2026:opencode-android-orchestrator:installation-manifest:v1",
4
+ "title": "OpenCode Android Orchestrator installation manifest",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schemaVersion",
9
+ "package",
10
+ "installation",
11
+ "backupDirectory",
12
+ "files"
13
+ ],
14
+ "$defs": {
15
+ "sha256": {
16
+ "type": "string",
17
+ "pattern": "^[a-f0-9]{64}$"
18
+ },
19
+ "timestamp": {
20
+ "type": "string",
21
+ "format": "date-time"
22
+ },
23
+ "relativePath": {
24
+ "type": "string",
25
+ "minLength": 1,
26
+ "pattern": "^(?!/)(?!\\s)(?!.*\\s$)(?!.*(?:^|/)\\.{1,2}(?:/|$))(?!.*\\\\)(?!.*//)(?!.*\\u0000)(?!.*\\/$).+"
27
+ },
28
+ "managedPath": {
29
+ "allOf": [
30
+ { "$ref": "#/$defs/relativePath" },
31
+ { "pattern": "^(?!\\.automation-plugin(?:/|$)).+" }
32
+ ]
33
+ }
34
+ },
35
+ "properties": {
36
+ "schemaVersion": { "const": 1 },
37
+ "package": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": ["name", "version"],
41
+ "properties": {
42
+ "name": { "type": "string", "minLength": 1 },
43
+ "version": { "type": "string", "minLength": 1 }
44
+ }
45
+ },
46
+ "installation": {
47
+ "type": "object",
48
+ "additionalProperties": false,
49
+ "required": [
50
+ "id",
51
+ "state",
52
+ "preparedAt",
53
+ "installedAt",
54
+ "rolledBackAt"
55
+ ],
56
+ "properties": {
57
+ "id": {
58
+ "type": "string",
59
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
60
+ },
61
+ "state": { "enum": ["prepared", "installed", "rolledBack"] },
62
+ "preparedAt": { "$ref": "#/$defs/timestamp" },
63
+ "installedAt": {
64
+ "oneOf": [
65
+ { "$ref": "#/$defs/timestamp" },
66
+ { "type": "null" }
67
+ ]
68
+ },
69
+ "rolledBackAt": {
70
+ "oneOf": [
71
+ { "$ref": "#/$defs/timestamp" },
72
+ { "type": "null" }
73
+ ]
74
+ }
75
+ },
76
+ "allOf": [
77
+ {
78
+ "if": { "properties": { "state": { "const": "prepared" } } },
79
+ "then": {
80
+ "properties": {
81
+ "installedAt": { "type": "null" },
82
+ "rolledBackAt": { "type": "null" }
83
+ }
84
+ }
85
+ },
86
+ {
87
+ "if": { "properties": { "state": { "const": "installed" } } },
88
+ "then": {
89
+ "properties": {
90
+ "installedAt": { "$ref": "#/$defs/timestamp" },
91
+ "rolledBackAt": { "type": "null" }
92
+ }
93
+ }
94
+ },
95
+ {
96
+ "if": { "properties": { "state": { "const": "rolledBack" } } },
97
+ "then": {
98
+ "properties": {
99
+ "rolledBackAt": { "$ref": "#/$defs/timestamp" }
100
+ }
101
+ }
102
+ }
103
+ ]
104
+ },
105
+ "backupDirectory": {
106
+ "allOf": [
107
+ { "$ref": "#/$defs/relativePath" },
108
+ {
109
+ "pattern": "^\\.automation-plugin/backups/[A-Za-z0-9][A-Za-z0-9._-]{0,127}$"
110
+ }
111
+ ]
112
+ },
113
+ "files": {
114
+ "type": "array",
115
+ "minItems": 1,
116
+ "items": {
117
+ "type": "object",
118
+ "additionalProperties": false,
119
+ "required": [
120
+ "path",
121
+ "source",
122
+ "strategy",
123
+ "sha256",
124
+ "size",
125
+ "mode",
126
+ "previous"
127
+ ],
128
+ "properties": {
129
+ "path": { "$ref": "#/$defs/managedPath" },
130
+ "source": { "$ref": "#/$defs/relativePath" },
131
+ "strategy": { "enum": ["copy", "generate", "merge"] },
132
+ "sha256": { "$ref": "#/$defs/sha256" },
133
+ "size": { "type": "integer", "minimum": 0 },
134
+ "mode": { "type": "integer", "minimum": 0, "maximum": 511 },
135
+ "previous": {
136
+ "type": "object",
137
+ "additionalProperties": false,
138
+ "required": [
139
+ "existed",
140
+ "sha256",
141
+ "size",
142
+ "mode",
143
+ "backupPath"
144
+ ],
145
+ "properties": {
146
+ "existed": { "type": "boolean" },
147
+ "sha256": {
148
+ "oneOf": [
149
+ { "$ref": "#/$defs/sha256" },
150
+ { "type": "null" }
151
+ ]
152
+ },
153
+ "size": {
154
+ "oneOf": [
155
+ { "type": "integer", "minimum": 0 },
156
+ { "type": "null" }
157
+ ]
158
+ },
159
+ "mode": {
160
+ "oneOf": [
161
+ { "type": "integer", "minimum": 0, "maximum": 511 },
162
+ { "type": "null" }
163
+ ]
164
+ },
165
+ "backupPath": {
166
+ "oneOf": [
167
+ { "$ref": "#/$defs/relativePath" },
168
+ { "type": "null" }
169
+ ]
170
+ }
171
+ },
172
+ "allOf": [
173
+ {
174
+ "if": { "properties": { "existed": { "const": false } } },
175
+ "then": {
176
+ "properties": {
177
+ "sha256": { "type": "null" },
178
+ "size": { "type": "null" },
179
+ "mode": { "type": "null" },
180
+ "backupPath": { "type": "null" }
181
+ }
182
+ }
183
+ },
184
+ {
185
+ "if": { "properties": { "existed": { "const": true } } },
186
+ "then": {
187
+ "properties": {
188
+ "sha256": { "$ref": "#/$defs/sha256" },
189
+ "size": { "type": "integer", "minimum": 0 },
190
+ "mode": { "type": "integer", "minimum": 0, "maximum": 511 },
191
+ "backupPath": { "$ref": "#/$defs/relativePath" }
192
+ }
193
+ }
194
+ }
195
+ ]
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,171 @@
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
+ approval="${2:-}"
10
+ if [[ "$#" -ne 2 ]]; then
11
+ printf 'Usage: %s TASK-ID "ABORT-APPROVAL"\n' "$0" >&2
12
+ exit 2
13
+ fi
14
+
15
+ automation_validate_task_id "$task_id"
16
+ automation_require_orchestrated
17
+ automation_require_approval abort "$approval"
18
+
19
+ current_state="$(automation_read_state "$task_id")"
20
+ case "$current_state" in
21
+ PREPARING|PENDING|CODING|READY_FOR_REVIEW|REVIEWING|CHANGES_REQUESTED|AWAITING_HUMAN|BLOCKED|TEST_FAILED|NEEDS_HUMAN|INTEGRATION_BLOCKED) ;;
22
+ *) automation_die "cannot abort $task_id from state $current_state" ;;
23
+ esac
24
+
25
+ origin_file="$(automation_origin_path "$task_id")"
26
+ workspace_file="$(automation_workspace_path "$task_id")"
27
+ evidence_dir="$(automation_evidence_path "$task_id")"
28
+ [[ -f "$origin_file" && -f "$workspace_file" ]] || automation_die "abort metadata is incomplete for $task_id"
29
+
30
+ source_root="$(jq -er '.sourceRoot' "$origin_file")"
31
+ original_branch="$(jq -er '.originalBranch' "$origin_file")"
32
+ task_root="$(automation_workspace_task_root "$workspace_file")"
33
+ task_branch="$(jq -er '.taskBranch' "$workspace_file")"
34
+ workspace_strategy="$(automation_workspace_strategy "$workspace_file")"
35
+ contract="$(automation_contract_path "$task_id")"
36
+ original_ref="refs/heads/$original_branch"
37
+
38
+ [[ "$source_root" == "$AUTOMATION_ROOT" ]] || automation_die "abort must run from the recorded source root"
39
+ automation_require_repository_lease "$task_id" "$source_root" "$workspace_strategy"
40
+ git -C "$source_root" show-ref --verify --quiet "$original_ref" || automation_die "recorded original branch no longer exists"
41
+
42
+ product_commit="$(jq -r '.productCommit // ""' "$workspace_file")"
43
+ original_head="$(git -C "$source_root" rev-parse "$original_ref")"
44
+ if [[ -n "$product_commit" && "$original_head" == "$product_commit" ]]; then
45
+ automation_die "the product commit is already on the original branch; finalize recovery instead of aborting"
46
+ fi
47
+
48
+ automation_acquire_run_lock "$task_id"
49
+ trap 'automation_release_run_lock' EXIT
50
+
51
+ recovery_commit=""
52
+ planning_only_archived_without_commit=false
53
+ : > "$evidence_dir/abort-status.txt"
54
+ : > "$evidence_dir/aborted.diff"
55
+ if [[ -d "$task_root" ]]; then
56
+ task_current_branch="$(automation_current_branch "$task_root")"
57
+ if [[ "$task_current_branch" == "$task_branch" ]]; then
58
+ automation_assert_planning_artifacts_sealed "$task_id" "$task_root"
59
+ changed_paths=()
60
+ changed_path_count=0
61
+ product_path_count=0
62
+ while IFS= read -r path; do
63
+ if [[ -n "$path" ]]; then
64
+ changed_paths+=("$path")
65
+ changed_path_count=$((changed_path_count + 1))
66
+ if ! automation_is_planning_artifact "$task_id" "$path"; then
67
+ product_path_count=$((product_path_count + 1))
68
+ fi
69
+ fi
70
+ done < <(automation_changed_paths_at "$task_root")
71
+
72
+ git -C "$task_root" status --short --branch > "$evidence_dir/abort-status.txt"
73
+ {
74
+ git -C "$task_root" diff --binary HEAD --
75
+ while IFS= read -r path; do
76
+ [[ -n "$path" ]] || continue
77
+ git -C "$task_root" diff --binary --no-index -- /dev/null "$task_root/$path" || [[ "$?" -eq 1 ]]
78
+ done < <(git -C "$task_root" ls-files --others --exclude-standard | LC_ALL=C sort)
79
+ } > "$evidence_dir/aborted.diff"
80
+
81
+ if [[ "$changed_path_count" -gt 0 ]]; then
82
+ for path in "${changed_paths[@]}"; do
83
+ if automation_is_planning_artifact "$task_id" "$path"; then
84
+ continue
85
+ fi
86
+ automation_array_matches_path "$contract" '.allowedPaths' "$path" || \
87
+ automation_die "abort refuses to archive an out-of-contract path: $path"
88
+ if automation_array_matches_path "$contract" '.forbiddenPaths' "$path" || \
89
+ automation_array_matches_path "$AUTOMATION_CONFIG" '.protectedPaths' "$path"; then
90
+ automation_die "abort refuses to archive a protected path: $path"
91
+ fi
92
+ done
93
+ if [[ "$product_path_count" -gt 0 ]]; then
94
+ git -C "$task_root" add -- "${changed_paths[@]}"
95
+ git -C "$task_root" commit -m "Archive aborted work for $task_id"
96
+ recovery_commit="$(git -C "$task_root" rev-parse HEAD)"
97
+ else
98
+ plan_rel="$(jq -er '.planPath' "$origin_file")"
99
+ contract_rel="$(jq -er '.contractPath' "$origin_file")"
100
+ for planning_path in "$plan_rel" "$contract_rel"; do
101
+ if git -C "$task_root" ls-files --error-unmatch -- "$planning_path" >/dev/null 2>&1; then
102
+ automation_die "abort refuses to discard a tracked planning artifact without product changes: $planning_path"
103
+ fi
104
+ rm -f "$task_root/$planning_path"
105
+ done
106
+ planning_only_archived_without_commit=true
107
+ fi
108
+ fi
109
+ if [[ "$changed_path_count" -eq 0 ]]; then
110
+ recovery_commit="$(git -C "$task_root" rev-parse HEAD)"
111
+ fi
112
+ automation_worktree_is_clean "$task_root" || automation_die "task root is not clean after archival"
113
+ elif [[ "$workspace_strategy" == "inPlaceExclusive" && "$task_current_branch" == "$original_branch" ]]; then
114
+ automation_worktree_is_clean "$source_root" || automation_die "source root is dirty after a partial abort; manual recovery is required"
115
+ recovery_commit="$(git -C "$source_root" rev-parse "refs/heads/$task_branch")"
116
+ else
117
+ automation_die "task root is on unexpected branch $task_current_branch"
118
+ fi
119
+ else
120
+ [[ "$workspace_strategy" == "isolatedWorktree" ]] || automation_die "in-place task root is missing"
121
+ recovery_commit="$(git -C "$source_root" rev-parse "refs/heads/$task_branch")"
122
+ fi
123
+
124
+ if [[ "$workspace_strategy" == "inPlaceExclusive" ]]; then
125
+ if [[ "$(automation_current_branch "$source_root")" == "$task_branch" ]]; then
126
+ git -C "$source_root" switch "$original_branch"
127
+ fi
128
+ [[ "$(automation_current_branch "$source_root")" == "$original_branch" ]] || automation_die "source root did not return to the original branch"
129
+ elif [[ -d "$task_root" ]]; then
130
+ cleanup_log="$evidence_dir/cleanup.log"
131
+ git -C "$source_root" worktree remove "$task_root" >> "$cleanup_log" 2>&1
132
+ printf '%s removed aborted isolated task worktree %s\n' "$(automation_now)" "$task_root" >> "$cleanup_log"
133
+ fi
134
+
135
+ jq -n \
136
+ --arg taskId "$task_id" \
137
+ --arg abortedAt "$(automation_now)" \
138
+ --arg previousState "$current_state" \
139
+ --arg originalBranch "$original_branch" \
140
+ --arg originalHead "$(git -C "$source_root" rev-parse "$original_ref")" \
141
+ --arg taskBranch "$task_branch" \
142
+ --arg recoveryCommit "$recovery_commit" \
143
+ --arg workspaceStrategy "$workspace_strategy" \
144
+ --arg archivedDiffSha256 "$(automation_file_sha256 "$evidence_dir/aborted.diff")" \
145
+ --argjson planningOnlyArchivedWithoutCommit "$planning_only_archived_without_commit" \
146
+ '{taskId: $taskId, abortedAt: $abortedAt, previousState: $previousState,
147
+ originalBranch: $originalBranch, originalHead: $originalHead,
148
+ taskBranch: $taskBranch,
149
+ recoveryCommit: ($recoveryCommit | if length == 0 then null else . end),
150
+ workspaceStrategy: $workspaceStrategy,
151
+ planningOnlyArchivedWithoutCommit: $planningOnlyArchivedWithoutCommit,
152
+ archivedDiffSha256: $archivedDiffSha256, pushed: false}' \
153
+ | automation_record_json "$evidence_dir/abort.json"
154
+
155
+ jq -nc \
156
+ --arg taskId "$task_id" \
157
+ --arg kind "abort" \
158
+ --arg at "$(automation_now)" \
159
+ --arg previousState "$current_state" \
160
+ --arg originalBranch "$original_branch" \
161
+ --arg recoveryCommit "$recovery_commit" \
162
+ '{taskId: $taskId, kind: $kind, at: $at,
163
+ previousState: $previousState, originalBranch: $originalBranch,
164
+ recoveryCommit: ($recoveryCommit | if length == 0 then null else . end)}' \
165
+ | automation_append_json "$(automation_approvals_path "$task_id")"
166
+
167
+ automation_transition_state "$task_id" "$current_state" "ABORTED" "human" "human aborted the task after deterministic archival"
168
+ automation_release_repository_lease "$task_id"
169
+ automation_release_run_lock
170
+ trap - EXIT
171
+ jq . "$evidence_dir/abort.json"
@@ -0,0 +1,263 @@
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
+ approval="${2:-}"
10
+ if [[ "$#" -ne 2 ]]; then
11
+ printf 'Usage: %s TASK-ID "FINAL-ACCEPTANCE"\n' "$0" >&2
12
+ exit 2
13
+ fi
14
+
15
+ automation_validate_task_id "$task_id"
16
+ automation_require_orchestrated
17
+ automation_require_approval acceptance "$approval"
18
+ [[ "$(automation_read_state "$task_id")" == "AWAITING_HUMAN" ]] || automation_die "$task_id is not awaiting human acceptance"
19
+ [[ "$(automation_config_value '.pushAfterAcceptance')" == "false" ]] || automation_die "automatic push is forbidden"
20
+ [[ "$(automation_config_value '.originalBranchDriftPolicy')" == "block" ]] || automation_die "unsupported original branch drift policy"
21
+
22
+ origin_file="$(automation_origin_path "$task_id")"
23
+ workspace_file="$(automation_workspace_path "$task_id")"
24
+ evidence_dir="$(automation_evidence_path "$task_id")"
25
+ ready_file="$evidence_dir/ready.json"
26
+ review_file="$evidence_dir/review.json"
27
+ [[ -f "$origin_file" && -f "$workspace_file" && -f "$ready_file" && -f "$review_file" ]] || automation_die "integration evidence is incomplete"
28
+
29
+ source_root="$(jq -er '.sourceRoot' "$origin_file")"
30
+ original_branch="$(jq -er '.originalBranch' "$origin_file")"
31
+ baseline_head="$(jq -er '.baselineHead' "$workspace_file")"
32
+ task_root="$(automation_workspace_task_root "$workspace_file")"
33
+ task_branch="$(jq -er '.taskBranch' "$workspace_file")"
34
+ workspace_strategy="$(automation_workspace_strategy "$workspace_file")"
35
+ original_ref="refs/heads/$original_branch"
36
+ task_ref="refs/heads/$task_branch"
37
+
38
+ [[ "$source_root" == "$AUTOMATION_ROOT" ]] || automation_die "final acceptance must run from the recorded source root"
39
+ [[ -d "$task_root" ]] || automation_die "task root is missing: $task_root"
40
+ [[ "$task_branch" != "$original_branch" ]] || automation_die "task branch must differ from the recorded original branch"
41
+ git -C "$source_root" show-ref --verify --quiet "$task_ref" || automation_die "recorded task branch no longer exists: $task_branch"
42
+ [[ "$(automation_current_branch "$task_root")" == "$task_branch" ]] || automation_die "task branch identity changed"
43
+ [[ "$(git -C "$task_root" rev-parse HEAD)" == "$baseline_head" ]] || automation_die "task branch HEAD changed before deterministic commit"
44
+ automation_require_repository_lease "$task_id" "$source_root" "$workspace_strategy"
45
+
46
+ case "$workspace_strategy" in
47
+ inPlaceExclusive)
48
+ [[ "$task_root" == "$source_root" ]] || automation_die "in-place task root must equal the source root"
49
+ ;;
50
+ isolatedWorktree)
51
+ [[ "$task_root" != "$source_root" ]] || automation_die "isolated task root must differ from the source root"
52
+ [[ "$(automation_current_branch "$source_root")" == "$original_branch" ]] || automation_die "source root is no longer on $original_branch"
53
+ automation_worktree_is_clean "$source_root" || automation_die "source root must be clean before integration"
54
+ ;;
55
+ *)
56
+ automation_die "unsupported workspace strategy: $workspace_strategy"
57
+ ;;
58
+ esac
59
+
60
+ git -C "$source_root" show-ref --verify --quiet "$original_ref" || automation_die "recorded original branch no longer exists: $original_branch"
61
+ source_head="$(git -C "$source_root" rev-parse "$original_ref")"
62
+ git -C "$source_root" merge-base --is-ancestor "$baseline_head" "$source_head" || automation_die "original branch no longer contains the recorded pre-task baseline"
63
+
64
+ automation_assert_planning_artifacts_sealed "$task_id" "$task_root"
65
+ sealed_diff_sha="$(automation_worktree_diff_sha "$task_root")"
66
+ [[ "$sealed_diff_sha" == "$(jq -er '.diffSha256' "$ready_file")" ]] || automation_die "task diff changed after quality gate"
67
+ [[ "$sealed_diff_sha" == "$(jq -er '.diffSha256' "$review_file")" ]] || automation_die "task diff changed after review"
68
+ [[ "$(jq -er '.decision' "$review_file")" == "APPROVED" ]] || automation_die "latest review is not approved"
69
+
70
+ (
71
+ cd "$task_root"
72
+ ./scripts/automation/scope-gate.sh "$task_id" >/dev/null
73
+ ./scripts/automation/acceptance-report.sh "$task_id" >/dev/null
74
+ )
75
+ report_file="$evidence_dir/acceptance-report.json"
76
+ [[ "$(jq -er '.sealedDiffSha256' "$report_file")" == "$sealed_diff_sha" ]] || automation_die "acceptance report is stale"
77
+
78
+ automation_acquire_run_lock "$task_id"
79
+ integration_complete=0
80
+ integration_exit() {
81
+ local exit_code=$?
82
+ trap - EXIT
83
+ if [[ "$integration_complete" != "1" ]] && [[ -f "$(automation_state_path "$task_id")" ]] && \
84
+ [[ "$(automation_read_state "$task_id" 2>/dev/null || true)" == "INTEGRATING" ]]; then
85
+ jq -n \
86
+ --arg taskId "$task_id" \
87
+ --arg failedAt "$(automation_now)" \
88
+ --arg originalBranch "$original_branch" \
89
+ --arg originalHead "$(git -C "$source_root" rev-parse "$original_ref" 2>/dev/null || true)" \
90
+ --arg productCommit "$(jq -r '.productCommit // ""' "$workspace_file" 2>/dev/null || true)" \
91
+ --argjson exitCode "$exit_code" \
92
+ '{taskId: $taskId, failedAt: $failedAt, exitCode: $exitCode,
93
+ originalBranch: $originalBranch, originalHead: $originalHead,
94
+ productCommit: ($productCommit | if length == 0 then null else . end),
95
+ message: "Integration stopped. Inspect the recorded refs before recovery; no automatic conflict resolution was attempted."}' \
96
+ | automation_record_json "$evidence_dir/integration-failure.json" || true
97
+ automation_transition_state "$task_id" "INTEGRATING" "INTEGRATION_BLOCKED" "integrator" "candidate verification or fast-forward failed" || true
98
+ fi
99
+ automation_release_run_lock
100
+ exit "$exit_code"
101
+ }
102
+ trap integration_exit EXIT
103
+
104
+ jq -nc \
105
+ --arg taskId "$task_id" \
106
+ --arg kind "acceptance" \
107
+ --arg at "$(automation_now)" \
108
+ --arg originalBranch "$original_branch" \
109
+ --arg sealedDiffSha256 "$sealed_diff_sha" \
110
+ '{taskId: $taskId, kind: $kind, at: $at,
111
+ originalBranch: $originalBranch, sealedDiffSha256: $sealedDiffSha256}' \
112
+ | automation_append_json "$(automation_approvals_path "$task_id")"
113
+ jq -n \
114
+ --arg taskId "$task_id" \
115
+ --arg acceptedAt "$(automation_now)" \
116
+ --arg originalBranch "$original_branch" \
117
+ --arg sealedDiffSha256 "$sealed_diff_sha" \
118
+ '{taskId: $taskId, acceptedAt: $acceptedAt,
119
+ originalBranch: $originalBranch, sealedDiffSha256: $sealedDiffSha256}' \
120
+ | automation_record_json "$evidence_dir/acceptance.json"
121
+
122
+ automation_transition_state "$task_id" "AWAITING_HUMAN" "INTEGRATING" "integrator" "human accepted the sealed diff for the recorded original branch"
123
+
124
+ product_paths=()
125
+ while IFS= read -r path; do
126
+ [[ -n "$path" ]] && product_paths+=("$path")
127
+ done < <(automation_product_changed_paths_at "$task_id" "$task_root")
128
+ [[ "${#product_paths[@]}" -gt 0 ]] || automation_die "no product paths remain to commit with the planning artifacts"
129
+
130
+ commit_paths=()
131
+ while IFS= read -r path; do
132
+ [[ -n "$path" ]] && commit_paths+=("$path")
133
+ done < <(automation_changed_paths_at "$task_root")
134
+ [[ "${#commit_paths[@]}" -ge 3 ]] || automation_die "final commit must contain product changes and both planning artifacts"
135
+ git -C "$task_root" add -- "${commit_paths[@]}"
136
+ title="$(jq -er '.title' "$task_root/automation/tasks/$task_id.json")"
137
+ git -C "$task_root" commit -m "Implement $title ($task_id)"
138
+ product_commit="$(git -C "$task_root" rev-parse HEAD)"
139
+ automation_worktree_is_clean "$task_root" || automation_die "task root is dirty after the combined task commit"
140
+ git -C "$task_root" merge-base --is-ancestor "$baseline_head" "$product_commit" || automation_die "combined task commit is not based on the recorded pre-task baseline"
141
+ [[ "$(git -C "$task_root" rev-list --count "$baseline_head..$product_commit")" -eq 1 ]] || \
142
+ automation_die "task history must contain exactly one combined product-and-planning commit"
143
+
144
+ jq \
145
+ --arg contractCommit "$product_commit" \
146
+ --arg committedAt "$(automation_now)" \
147
+ '.contractCommit = $contractCommit |
148
+ .planningArtifactsCommittedWithProduct = true |
149
+ .planningArtifactsCommittedAt = $committedAt' \
150
+ "$origin_file" | automation_record_json "$origin_file"
151
+
152
+ jq \
153
+ --arg productCommit "$product_commit" \
154
+ --arg candidateHead "$product_commit" \
155
+ --arg sourceHeadAtIntegration "$source_head" \
156
+ --arg integrationMethod "$workspace_strategy-fast-forward" \
157
+ --arg updatedAt "$(automation_now)" \
158
+ '.productCommit = $productCommit |
159
+ .candidateHead = $candidateHead |
160
+ .sourceHeadAtIntegration = $sourceHeadAtIntegration |
161
+ .integrationMethod = $integrationMethod |
162
+ .updatedAt = $updatedAt' \
163
+ "$workspace_file" | automation_record_json "$workspace_file"
164
+
165
+ [[ "$source_head" == "$baseline_head" ]] || automation_die "original branch drifted from $baseline_head to $source_head; policy requires manual resolution"
166
+ [[ "$(automation_file_sha256 "$task_root/automation/tasks/$task_id.json")" == "$(jq -er '.contractSha256' "$origin_file")" ]] || \
167
+ automation_die "approved contract changed on the verified candidate"
168
+
169
+ set +e
170
+ (
171
+ cd "$task_root"
172
+ ./scripts/automation/verify-integration.sh "$task_id" "$baseline_head" "$product_commit"
173
+ ) 2>&1 | tee "$evidence_dir/integration-verification.log"
174
+ verification_status=${PIPESTATUS[0]}
175
+ set -e
176
+ [[ "$verification_status" -eq 0 ]] || automation_die "integration verification failed with exit $verification_status"
177
+
178
+ [[ "$(git -C "$source_root" rev-parse "$original_ref")" == "$baseline_head" ]] || automation_die "original branch advanced during candidate verification"
179
+ if [[ "$workspace_strategy" == "inPlaceExclusive" ]]; then
180
+ [[ "$(automation_current_branch "$source_root")" == "$task_branch" ]] || automation_die "in-place task branch changed during verification"
181
+ git -C "$source_root" switch "$original_branch"
182
+ else
183
+ [[ "$(automation_current_branch "$source_root")" == "$original_branch" ]] || automation_die "source branch changed during candidate verification"
184
+ [[ "$(git -C "$source_root" rev-parse HEAD)" == "$baseline_head" ]] || automation_die "source HEAD changed during candidate verification"
185
+ automation_worktree_is_clean "$source_root" || automation_die "source root became dirty during candidate verification"
186
+ fi
187
+
188
+ git -C "$source_root" merge --ff-only "$product_commit"
189
+ integrated_head="$(git -C "$source_root" rev-parse HEAD)"
190
+ [[ "$integrated_head" == "$product_commit" ]] || automation_die "original branch did not reach the verified combined task commit"
191
+ integration_method="$workspace_strategy-fast-forward"
192
+ cleanup_log="$evidence_dir/cleanup.log"
193
+ task_worktree_disposition="source-root-reused"
194
+
195
+ if [[ "$workspace_strategy" == "isolatedWorktree" ]]; then
196
+ if [[ "$(automation_config_value '.autoCleanupWorktrees')" == "true" ]]; then
197
+ git -C "$source_root" worktree remove "$task_root" >> "$cleanup_log" 2>&1 || \
198
+ automation_die "could not remove isolated task worktree: $task_root"
199
+ printf '%s removed isolated task worktree %s\n' "$(automation_now)" "$task_root" >> "$cleanup_log"
200
+ task_worktree_disposition="removed"
201
+ else
202
+ git -C "$task_root" switch --detach "$product_commit" >> "$cleanup_log" 2>&1 || \
203
+ automation_die "could not detach retained isolated task worktree: $task_root"
204
+ printf '%s retained isolated task worktree in detached state at %s\n' \
205
+ "$(automation_now)" "$product_commit" >> "$cleanup_log"
206
+ task_worktree_disposition="retained-detached"
207
+ fi
208
+ fi
209
+
210
+ [[ "$(git -C "$source_root" rev-parse "$task_ref")" == "$product_commit" ]] || \
211
+ automation_die "task branch no longer points to the verified combined task commit"
212
+ [[ "$(automation_current_branch "$source_root")" == "$original_branch" ]] || \
213
+ automation_die "source root must be on the original branch before task branch deletion"
214
+ git -C "$source_root" branch -d -- "$task_branch" >> "$cleanup_log" 2>&1 || \
215
+ automation_die "could not safely delete integrated task branch: $task_branch"
216
+ if git -C "$source_root" show-ref --verify --quiet "$task_ref"; then
217
+ automation_die "integrated task branch still exists after deletion: $task_branch"
218
+ fi
219
+ task_branch_deleted_at="$(automation_now)"
220
+ printf '%s deleted integrated local task branch %s at %s\n' \
221
+ "$task_branch_deleted_at" "$task_branch" "$product_commit" >> "$cleanup_log"
222
+
223
+ jq \
224
+ --arg taskBranchDeletedAt "$task_branch_deleted_at" \
225
+ --arg taskWorktreeDisposition "$task_worktree_disposition" \
226
+ --arg updatedAt "$(automation_now)" \
227
+ '.taskBranchDeleted = true |
228
+ .taskBranchDeletedAt = $taskBranchDeletedAt |
229
+ .taskWorktreeDisposition = $taskWorktreeDisposition |
230
+ .updatedAt = $updatedAt' \
231
+ "$workspace_file" | automation_record_json "$workspace_file"
232
+
233
+ jq -n \
234
+ --arg taskId "$task_id" \
235
+ --arg integratedAt "$(automation_now)" \
236
+ --arg originalBranch "$original_branch" \
237
+ --arg sourceHeadBeforeIntegration "$source_head" \
238
+ --arg productCommit "$product_commit" \
239
+ --arg integratedHead "$integrated_head" \
240
+ --arg method "$integration_method" \
241
+ --arg workspaceStrategy "$workspace_strategy" \
242
+ --arg taskBranch "$task_branch" \
243
+ --arg taskBranchDeletedAt "$task_branch_deleted_at" \
244
+ --arg taskWorktreeDisposition "$task_worktree_disposition" \
245
+ --argjson verificationExitCode "$verification_status" \
246
+ '{taskId: $taskId, integratedAt: $integratedAt,
247
+ originalBranch: $originalBranch,
248
+ sourceHeadBeforeIntegration: $sourceHeadBeforeIntegration,
249
+ productCommit: $productCommit, integratedHead: $integratedHead,
250
+ method: $method, workspaceStrategy: $workspaceStrategy,
251
+ taskBranch: $taskBranch, taskBranchDeleted: true,
252
+ taskBranchDeletedAt: $taskBranchDeletedAt,
253
+ taskWorktreeDisposition: $taskWorktreeDisposition,
254
+ verificationExitCode: $verificationExitCode, pushed: false}' \
255
+ | automation_record_json "$evidence_dir/integration.json"
256
+
257
+ automation_transition_state "$task_id" "INTEGRATING" "COMPLETED" "integrator" "verified combined task commit fast-forwarded into the recorded original branch and the integrated local task branch was deleted; not pushed"
258
+ integration_complete=1
259
+
260
+ automation_release_repository_lease "$task_id"
261
+ automation_release_run_lock
262
+ trap - EXIT
263
+ jq . "$evidence_dir/integration.json"