@frankzhang2026/opencode-android-orchestrator 0.1.0 → 0.2.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.
- package/CHANGELOG.md +75 -2
- package/LICENSE +21 -0
- package/README.md +391 -6
- package/THIRD_PARTY_NOTICES.md +119 -0
- package/dist/cli.js +243 -1
- package/dist/cli.js.map +1 -1
- package/dist/compatibility/hooks.d.ts +23 -0
- package/dist/compatibility/hooks.d.ts.map +1 -0
- package/dist/compatibility/hooks.js +44 -0
- package/dist/compatibility/hooks.js.map +1 -0
- package/dist/compatibility/versions.d.ts +19 -0
- package/dist/compatibility/versions.d.ts.map +1 -1
- package/dist/compatibility/versions.js +116 -0
- package/dist/compatibility/versions.js.map +1 -1
- package/dist/doctor/index.d.ts +36 -0
- package/dist/doctor/index.d.ts.map +1 -0
- package/dist/doctor/index.js +289 -0
- package/dist/doctor/index.js.map +1 -0
- package/dist/doctor/installation.d.ts +4 -0
- package/dist/doctor/installation.d.ts.map +1 -0
- package/dist/doctor/installation.js +472 -0
- package/dist/doctor/installation.js.map +1 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/installer/adaptive-templates.d.ts +56 -0
- package/dist/installer/adaptive-templates.d.ts.map +1 -0
- package/dist/installer/adaptive-templates.js +167 -0
- package/dist/installer/adaptive-templates.js.map +1 -0
- package/dist/installer/agents-config.d.ts +19 -0
- package/dist/installer/agents-config.d.ts.map +1 -0
- package/dist/installer/agents-config.js +147 -0
- package/dist/installer/agents-config.js.map +1 -0
- package/dist/installer/android-project.d.ts +36 -0
- package/dist/installer/android-project.d.ts.map +1 -0
- package/dist/installer/android-project.js +418 -0
- package/dist/installer/android-project.js.map +1 -0
- package/dist/installer/index.d.ts +8 -0
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +8 -1
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/init.d.ts +67 -0
- package/dist/installer/init.d.ts.map +1 -0
- package/dist/installer/init.js +389 -0
- package/dist/installer/init.js.map +1 -0
- package/dist/installer/install-manifest.d.ts +132 -0
- package/dist/installer/install-manifest.d.ts.map +1 -0
- package/dist/installer/install-manifest.js +1047 -0
- package/dist/installer/install-manifest.js.map +1 -0
- package/dist/installer/opencode-config.d.ts +31 -0
- package/dist/installer/opencode-config.d.ts.map +1 -0
- package/dist/installer/opencode-config.js +296 -0
- package/dist/installer/opencode-config.js.map +1 -0
- package/dist/installer/uninstall.d.ts +70 -0
- package/dist/installer/uninstall.d.ts.map +1 -0
- package/dist/installer/uninstall.js +770 -0
- package/dist/installer/uninstall.js.map +1 -0
- package/dist/installer/upgrade.d.ts +96 -0
- package/dist/installer/upgrade.d.ts.map +1 -0
- package/dist/installer/upgrade.js +1106 -0
- package/dist/installer/upgrade.js.map +1 -0
- package/dist/opencode-plugin.d.ts +2 -0
- package/dist/opencode-plugin.d.ts.map +1 -0
- package/dist/opencode-plugin.js +4 -0
- package/dist/opencode-plugin.js.map +1 -0
- package/dist/plugin/index.d.ts +7 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +23 -6
- package/dist/plugin/index.js.map +1 -1
- package/dist/tools/index.d.ts +35 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +197 -1
- package/dist/tools/index.js.map +1 -1
- package/docs/MIGRATION.md +173 -0
- package/docs/SECURITY.md +212 -0
- package/docs/TROUBLESHOOTING.md +172 -0
- package/package.json +23 -5
- package/templates/.opencode/agents/scheduled-coder.md +115 -0
- package/templates/.opencode/agents/scheduled-planner.md +166 -0
- package/templates/.opencode/agents/scheduled-reviewer.md +88 -0
- package/templates/.opencode/commands/abort-task.md +20 -0
- package/templates/.opencode/commands/acceptance.md +20 -0
- package/templates/.opencode/commands/change.md +23 -0
- package/templates/.opencode/commands/resume-review.md +19 -0
- package/templates/.opencode/skills/scheduled-quality-coder/SKILL.md +93 -0
- package/templates/.opencode/skills/scheduled-quality-orchestrator/SKILL.md +216 -0
- package/templates/.opencode/skills/scheduled-quality-reviewer/SKILL.md +68 -0
- package/templates/AGENTS.md.fragment +19 -0
- package/templates/README.md +52 -9
- package/templates/automation/config.json +50 -0
- package/templates/automation/config.schema.json +163 -0
- package/templates/automation/task-contract.schema.json +71 -0
- package/templates/automation/tasks/TASK-TEMPLATE.json.example +53 -0
- package/templates/docs/plans/README.md +26 -0
- package/templates/installation-manifest.schema.json +201 -0
- package/templates/scripts/automation/abort-task.sh +171 -0
- package/templates/scripts/automation/accept-and-integrate.sh +263 -0
- package/templates/scripts/automation/acceptance-report.sh +132 -0
- package/templates/scripts/automation/approve-and-run.sh +175 -0
- package/templates/scripts/automation/begin-review.sh +18 -0
- package/templates/scripts/automation/block-task.sh +26 -0
- package/templates/scripts/automation/claim-task.sh +74 -0
- package/templates/scripts/automation/integration-scope-gate.sh +103 -0
- package/templates/scripts/automation/lib.sh +697 -0
- package/templates/scripts/automation/orchestrate-task.sh +119 -0
- package/templates/scripts/automation/preflight.sh +206 -0
- package/templates/scripts/automation/prepare-contract-review.sh +101 -0
- package/templates/scripts/automation/quality-gate.sh +72 -0
- package/templates/scripts/automation/queue-task.sh +35 -0
- package/templates/scripts/automation/record-red.sh +57 -0
- package/templates/scripts/automation/resume-review-fix.sh +32 -0
- package/templates/scripts/automation/resume-review.sh +120 -0
- package/templates/scripts/automation/scope-gate.sh +110 -0
- package/templates/scripts/automation/select-task.sh +34 -0
- package/templates/scripts/automation/shadow-run.sh +24 -0
- package/templates/scripts/automation/show-acceptance-review.sh +111 -0
- package/templates/scripts/automation/status.sh +125 -0
- package/templates/scripts/automation/submit-review.sh +73 -0
- package/templates/scripts/automation/tests/run-tests.sh +699 -0
- package/templates/scripts/automation/transition-state.sh +21 -0
- package/templates/scripts/automation/validate-contract.sh +87 -0
- package/templates/scripts/automation/verify-integration.sh +37 -0
- package/templates/scripts/automation/verify-task.sh +54 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Shared helpers for the scheduled coding quality gate. This file is sourced by
|
|
4
|
+
# command scripts; it intentionally does not enable set -e for its callers.
|
|
5
|
+
|
|
6
|
+
AUTOMATION_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
|
|
8
|
+
if [[ -n "${AUTOMATION_PROJECT_ROOT:-}" ]]; then
|
|
9
|
+
if [[ "${AUTOMATION_TEST_MODE:-0}" != "1" ]]; then
|
|
10
|
+
printf 'ERROR: AUTOMATION_PROJECT_ROOT is reserved for the test suite.\n' >&2
|
|
11
|
+
return 1 2>/dev/null || exit 1
|
|
12
|
+
fi
|
|
13
|
+
AUTOMATION_ROOT="$(cd "$AUTOMATION_PROJECT_ROOT" && pwd)"
|
|
14
|
+
else
|
|
15
|
+
AUTOMATION_ROOT="$(git -C "$AUTOMATION_SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)" || {
|
|
16
|
+
printf 'ERROR: scripts must run inside a Git worktree.\n' >&2
|
|
17
|
+
return 1 2>/dev/null || exit 1
|
|
18
|
+
}
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
AUTOMATION_DIR="$AUTOMATION_ROOT/automation"
|
|
22
|
+
AUTOMATION_CONFIG="$AUTOMATION_DIR/config.json"
|
|
23
|
+
AUTOMATION_TASKS_DIR="$AUTOMATION_DIR/tasks"
|
|
24
|
+
|
|
25
|
+
automation_git_common_dir="$(git -C "$AUTOMATION_ROOT" rev-parse --git-common-dir 2>/dev/null)" || {
|
|
26
|
+
printf 'ERROR: unable to resolve Git common directory.\n' >&2
|
|
27
|
+
return 1 2>/dev/null || exit 1
|
|
28
|
+
}
|
|
29
|
+
if [[ "$automation_git_common_dir" != /* ]]; then
|
|
30
|
+
automation_git_common_dir="$AUTOMATION_ROOT/$automation_git_common_dir"
|
|
31
|
+
fi
|
|
32
|
+
AUTOMATION_GIT_COMMON_DIR="$(cd "$automation_git_common_dir" && pwd)"
|
|
33
|
+
unset automation_git_common_dir
|
|
34
|
+
|
|
35
|
+
if [[ -n "${AUTOMATION_RUNTIME_ROOT:-}" ]]; then
|
|
36
|
+
if [[ "${AUTOMATION_TEST_MODE:-0}" != "1" ]]; then
|
|
37
|
+
printf 'ERROR: AUTOMATION_RUNTIME_ROOT is reserved for the test suite.\n' >&2
|
|
38
|
+
return 1 2>/dev/null || exit 1
|
|
39
|
+
fi
|
|
40
|
+
elif [[ "${AUTOMATION_TEST_MODE:-0}" == "1" ]]; then
|
|
41
|
+
AUTOMATION_RUNTIME_ROOT="$AUTOMATION_DIR"
|
|
42
|
+
else
|
|
43
|
+
AUTOMATION_RUNTIME_ROOT="$AUTOMATION_GIT_COMMON_DIR/automation-runtime"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
AUTOMATION_STATE_DIR="$AUTOMATION_RUNTIME_ROOT/state"
|
|
47
|
+
AUTOMATION_EVIDENCE_DIR="$AUTOMATION_RUNTIME_ROOT/evidence"
|
|
48
|
+
AUTOMATION_LOCKS_DIR="$AUTOMATION_RUNTIME_ROOT/locks"
|
|
49
|
+
AUTOMATION_WORKSPACES_DIR="$AUTOMATION_RUNTIME_ROOT/workspaces"
|
|
50
|
+
|
|
51
|
+
automation_info() {
|
|
52
|
+
printf '[automation] %s\n' "$*"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
automation_warn() {
|
|
56
|
+
printf '[automation] WARN: %s\n' "$*" >&2
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
automation_die() {
|
|
60
|
+
printf '[automation] ERROR: %s\n' "$*" >&2
|
|
61
|
+
return 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
automation_now() {
|
|
65
|
+
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
automation_require_command() {
|
|
69
|
+
command -v "$1" >/dev/null 2>&1 || automation_die "required command is missing: $1"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
automation_require_layout() {
|
|
73
|
+
[[ -f "$AUTOMATION_CONFIG" ]] || automation_die "missing $AUTOMATION_CONFIG"
|
|
74
|
+
[[ -d "$AUTOMATION_TASKS_DIR" ]] || automation_die "missing $AUTOMATION_TASKS_DIR"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
automation_validate_config() {
|
|
78
|
+
automation_require_layout
|
|
79
|
+
jq -e '
|
|
80
|
+
def repository_path:
|
|
81
|
+
type == "string" and
|
|
82
|
+
length > 0 and
|
|
83
|
+
(startswith("/") | not) and
|
|
84
|
+
(test("^[A-Za-z]:[/\\\\]") | not) and
|
|
85
|
+
(test("(^|/)\\.\\.(/|$)") | not);
|
|
86
|
+
.schemaVersion == 3 and
|
|
87
|
+
(.enabled | type == "boolean") and
|
|
88
|
+
(.mode == "shadow" or .mode == "orchestrated") and
|
|
89
|
+
(.workspaceStrategy == "inPlaceExclusive" or .workspaceStrategy == "isolatedWorktree") and
|
|
90
|
+
.originalBranchDriftPolicy == "block" and
|
|
91
|
+
(.worktreeBase | type == "string") and
|
|
92
|
+
(.maxFixLoops | type == "number" and . >= 0 and . <= 1 and floor == .) and
|
|
93
|
+
(.maxReviewCycles | type == "number" and . >= 0 and . <= 2 and floor == .) and
|
|
94
|
+
(.maxReviewerRestarts | type == "number" and . >= 0 and . <= 3 and floor == .) and
|
|
95
|
+
(.autoCleanupWorktrees | type == "boolean") and
|
|
96
|
+
.pushAfterAcceptance == false and
|
|
97
|
+
(.approvalPhrases.proposal | type == "string" and length >= 4) and
|
|
98
|
+
(.approvalPhrases.contract | type == "string" and length >= 4) and
|
|
99
|
+
(.approvalPhrases.acceptance | type == "string" and length >= 4) and
|
|
100
|
+
(.approvalPhrases.abort | type == "string" and length >= 4) and
|
|
101
|
+
(.plugins | type == "object" and keys == ["superpowers"]) and
|
|
102
|
+
(.plugins.superpowers | type == "string" and length > 0) and
|
|
103
|
+
(.requiredSkills | type == "array" and length >= 6) and
|
|
104
|
+
(.androidProject as $project |
|
|
105
|
+
($project | type == "object") and
|
|
106
|
+
($project.name | type == "string" and length > 0) and
|
|
107
|
+
($project.gradleDsl == "kotlin" or $project.gradleDsl == "groovy" or $project.gradleDsl == "mixed") and
|
|
108
|
+
($project.settingsFile | repository_path) and
|
|
109
|
+
($project.primaryModule | type == "string" and test("^:(?:[A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)*)?$")) and
|
|
110
|
+
($project.modules | type == "array" and length > 0 and all(.[];
|
|
111
|
+
(.gradlePath | type == "string" and test("^:(?:[A-Za-z0-9_.-]+(?::[A-Za-z0-9_.-]+)*)?$")) and
|
|
112
|
+
(.directory | repository_path) and
|
|
113
|
+
(.buildFile | repository_path) and
|
|
114
|
+
(.dsl == "kotlin" or .dsl == "groovy") and
|
|
115
|
+
(.type == "application" or .type == "library" or .type == "dynamic-feature" or .type == "test" or .type == "asset-pack") and
|
|
116
|
+
(.namespace == null or (.namespace | type == "string" and length > 0)) and
|
|
117
|
+
(.applicationId == null or (.applicationId | type == "string" and length > 0)))) and
|
|
118
|
+
any($project.modules[]; .gradlePath == $project.primaryModule) and
|
|
119
|
+
($project.productionPaths | type == "array" and length > 0 and all(.[];
|
|
120
|
+
repository_path and test("(^|/)src/main/\\*\\*$"))) and
|
|
121
|
+
($project.testPaths | type == "array" and length > 0 and all(.[];
|
|
122
|
+
repository_path and test("(^|/)src/(?:test|androidTest)/\\*\\*$")))) and
|
|
123
|
+
(.protectedPaths | type == "array" and length > 0 and all(.[]; repository_path))
|
|
124
|
+
' "$AUTOMATION_CONFIG" >/dev/null || automation_die "automation/config.json is invalid"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
automation_require_orchestrated() {
|
|
128
|
+
automation_validate_config
|
|
129
|
+
[[ "$(automation_config_value '.enabled')" == "true" ]] || automation_die "automation is disabled"
|
|
130
|
+
[[ "$(automation_config_value '.mode')" == "orchestrated" ]] || automation_die "automation mode is not orchestrated"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
automation_ensure_runtime_layout() {
|
|
134
|
+
mkdir -p \
|
|
135
|
+
"$AUTOMATION_STATE_DIR" \
|
|
136
|
+
"$AUTOMATION_EVIDENCE_DIR" \
|
|
137
|
+
"$AUTOMATION_LOCKS_DIR" \
|
|
138
|
+
"$AUTOMATION_WORKSPACES_DIR"
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
automation_validate_task_id() {
|
|
142
|
+
local task_id="${1:-}"
|
|
143
|
+
[[ "$task_id" =~ ^TASK-[A-Z0-9-]+$ ]] || automation_die "invalid task ID: ${task_id:-<empty>}"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
automation_contract_path() {
|
|
147
|
+
local task_id="$1"
|
|
148
|
+
printf '%s/%s.json\n' "$AUTOMATION_TASKS_DIR" "$task_id"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
automation_contract_relative_path() {
|
|
152
|
+
local task_id="$1"
|
|
153
|
+
printf 'automation/tasks/%s.json\n' "$task_id"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
automation_state_path() {
|
|
157
|
+
local task_id="$1"
|
|
158
|
+
printf '%s/%s.json\n' "$AUTOMATION_STATE_DIR" "$task_id"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
automation_evidence_path() {
|
|
162
|
+
local task_id="$1"
|
|
163
|
+
printf '%s/%s\n' "$AUTOMATION_EVIDENCE_DIR" "$task_id"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
automation_workspace_path() {
|
|
167
|
+
local task_id="$1"
|
|
168
|
+
printf '%s/%s.json\n' "$AUTOMATION_WORKSPACES_DIR" "$task_id"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
automation_workspace_strategy() {
|
|
172
|
+
local workspace_file="$1"
|
|
173
|
+
jq -er '.workspaceStrategy // "isolatedWorktree"' "$workspace_file"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
automation_workspace_task_root() {
|
|
177
|
+
local workspace_file="$1"
|
|
178
|
+
jq -er '.taskRoot // .taskWorktree' "$workspace_file"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
automation_origin_path() {
|
|
182
|
+
local task_id="$1"
|
|
183
|
+
printf '%s/origin.json\n' "$(automation_evidence_path "$task_id")"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
automation_approvals_path() {
|
|
187
|
+
local task_id="$1"
|
|
188
|
+
printf '%s/approvals.jsonl\n' "$(automation_evidence_path "$task_id")"
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
automation_read_state() {
|
|
192
|
+
local task_id="$1"
|
|
193
|
+
local state_file
|
|
194
|
+
state_file="$(automation_state_path "$task_id")"
|
|
195
|
+
[[ -f "$state_file" ]] || automation_die "state does not exist for $task_id"
|
|
196
|
+
jq -er '.state' "$state_file"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
automation_config_value() {
|
|
200
|
+
local query="$1"
|
|
201
|
+
jq -r "$query" "$AUTOMATION_CONFIG"
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
automation_require_approval() {
|
|
205
|
+
local kind="$1"
|
|
206
|
+
local supplied="$2"
|
|
207
|
+
local expected
|
|
208
|
+
expected="$(jq -er --arg kind "$kind" '.approvalPhrases[$kind]' "$AUTOMATION_CONFIG")" || {
|
|
209
|
+
automation_die "unknown approval kind: $kind"
|
|
210
|
+
return 1
|
|
211
|
+
}
|
|
212
|
+
[[ "$supplied" == "$expected" ]] || automation_die "approval option token does not match the configured $kind confirmation"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
automation_file_sha256() {
|
|
216
|
+
local file="$1"
|
|
217
|
+
[[ -f "$file" ]] || automation_die "file does not exist: $file"
|
|
218
|
+
shasum -a 256 "$file" | awk '{print $1}'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
automation_current_branch() {
|
|
222
|
+
local root="${1:-$AUTOMATION_ROOT}"
|
|
223
|
+
local branch
|
|
224
|
+
branch="$(git -C "$root" symbolic-ref --quiet --short HEAD 2>/dev/null)" || {
|
|
225
|
+
automation_die "detached HEAD is not supported: $root"
|
|
226
|
+
return 1
|
|
227
|
+
}
|
|
228
|
+
printf '%s\n' "$branch"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
automation_path_matches() {
|
|
232
|
+
local path="$1"
|
|
233
|
+
local pattern="$2"
|
|
234
|
+
local prefix
|
|
235
|
+
|
|
236
|
+
if [[ "$pattern" == */ ]]; then
|
|
237
|
+
[[ "$path" == "$pattern"* ]]
|
|
238
|
+
return
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
if [[ "$pattern" == *'/**' ]]; then
|
|
242
|
+
prefix="${pattern:0:${#pattern}-3}"
|
|
243
|
+
[[ "$path" == "$prefix" || "$path" == "$prefix/"* ]]
|
|
244
|
+
return
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
case "$path" in
|
|
248
|
+
$pattern) return 0 ;;
|
|
249
|
+
*) return 1 ;;
|
|
250
|
+
esac
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
automation_array_matches_path() {
|
|
254
|
+
local json_file="$1"
|
|
255
|
+
local query="$2"
|
|
256
|
+
local path="$3"
|
|
257
|
+
local pattern
|
|
258
|
+
|
|
259
|
+
while IFS= read -r pattern; do
|
|
260
|
+
if automation_path_matches "$path" "$pattern"; then
|
|
261
|
+
return 0
|
|
262
|
+
fi
|
|
263
|
+
done < <(jq -r "$query[]" "$json_file")
|
|
264
|
+
|
|
265
|
+
return 1
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
automation_changed_paths_at() {
|
|
269
|
+
local root="$1"
|
|
270
|
+
{
|
|
271
|
+
git -C "$root" diff --name-only HEAD --
|
|
272
|
+
git -C "$root" ls-files --others --exclude-standard
|
|
273
|
+
} | LC_ALL=C sort -u
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
automation_changed_paths() {
|
|
277
|
+
automation_changed_paths_at "$AUTOMATION_ROOT"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
automation_assert_planning_artifacts_sealed() {
|
|
281
|
+
local task_id="$1"
|
|
282
|
+
local root="${2:-$AUTOMATION_ROOT}"
|
|
283
|
+
local origin_file contract_rel plan_rel contract_path plan_path
|
|
284
|
+
|
|
285
|
+
origin_file="$(automation_origin_path "$task_id")"
|
|
286
|
+
[[ -f "$origin_file" ]] || automation_die "origin evidence is missing for $task_id"
|
|
287
|
+
contract_rel="$(jq -er '.contractPath' "$origin_file")"
|
|
288
|
+
plan_rel="$(jq -er '.planPath' "$origin_file")"
|
|
289
|
+
contract_path="$root/$contract_rel"
|
|
290
|
+
plan_path="$root/$plan_rel"
|
|
291
|
+
|
|
292
|
+
[[ "$(automation_file_sha256 "$contract_path")" == "$(jq -er '.contractSha256' "$origin_file")" ]] || \
|
|
293
|
+
automation_die "approved contract changed after contract review"
|
|
294
|
+
[[ "$(automation_file_sha256 "$plan_path")" == "$(jq -er '.planSha256' "$origin_file")" ]] || \
|
|
295
|
+
automation_die "approved plan changed after contract review"
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
automation_is_planning_artifact() {
|
|
299
|
+
local task_id="$1"
|
|
300
|
+
local path="$2"
|
|
301
|
+
local origin_file contract_rel plan_rel
|
|
302
|
+
|
|
303
|
+
origin_file="$(automation_origin_path "$task_id")"
|
|
304
|
+
[[ -f "$origin_file" ]] || return 1
|
|
305
|
+
contract_rel="$(jq -er '.contractPath' "$origin_file")" || return 1
|
|
306
|
+
plan_rel="$(jq -er '.planPath' "$origin_file")" || return 1
|
|
307
|
+
[[ "$path" == "$contract_rel" || "$path" == "$plan_rel" ]]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
automation_product_changed_paths_at() {
|
|
311
|
+
local task_id="$1"
|
|
312
|
+
local root="${2:-$AUTOMATION_ROOT}"
|
|
313
|
+
local path
|
|
314
|
+
|
|
315
|
+
while IFS= read -r path; do
|
|
316
|
+
[[ -n "$path" ]] || continue
|
|
317
|
+
if ! automation_is_planning_artifact "$task_id" "$path"; then
|
|
318
|
+
printf '%s\n' "$path"
|
|
319
|
+
fi
|
|
320
|
+
done < <(automation_changed_paths_at "$root")
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
automation_product_changed_paths_between() {
|
|
324
|
+
local task_id="$1"
|
|
325
|
+
local base="$2"
|
|
326
|
+
local head="$3"
|
|
327
|
+
local root="${4:-$AUTOMATION_ROOT}"
|
|
328
|
+
local path
|
|
329
|
+
|
|
330
|
+
while IFS= read -r path; do
|
|
331
|
+
[[ -n "$path" ]] || continue
|
|
332
|
+
if ! automation_is_planning_artifact "$task_id" "$path"; then
|
|
333
|
+
printf '%s\n' "$path"
|
|
334
|
+
fi
|
|
335
|
+
done < <(git -C "$root" diff --name-only "$base" "$head" -- | LC_ALL=C sort -u)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
automation_changed_paths_between() {
|
|
339
|
+
local base="$1"
|
|
340
|
+
local head="$2"
|
|
341
|
+
git -C "$AUTOMATION_ROOT" diff --name-only "$base" "$head" -- | LC_ALL=C sort -u
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
automation_worktree_diff_sha() {
|
|
345
|
+
local root="${1:-$AUTOMATION_ROOT}"
|
|
346
|
+
local path
|
|
347
|
+
{
|
|
348
|
+
git -C "$root" diff --binary HEAD --
|
|
349
|
+
while IFS= read -r path; do
|
|
350
|
+
[[ -n "$path" ]] || continue
|
|
351
|
+
printf 'UNTRACKED %s\0' "$path"
|
|
352
|
+
git -C "$root" hash-object -- "$path"
|
|
353
|
+
done < <(git -C "$root" ls-files --others --exclude-standard | LC_ALL=C sort)
|
|
354
|
+
} | shasum -a 256 | awk '{print $1}'
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
automation_worktree_is_clean() {
|
|
358
|
+
local root="${1:-$AUTOMATION_ROOT}"
|
|
359
|
+
[[ -z "$(git -C "$root" status --porcelain --untracked-files=all)" ]]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
automation_create_lock_dir() {
|
|
363
|
+
local lock_dir="$1"
|
|
364
|
+
local label="$2"
|
|
365
|
+
local owner_pid=""
|
|
366
|
+
if mkdir "$lock_dir" 2>/dev/null; then
|
|
367
|
+
return 0
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
if [[ -f "$lock_dir/pid" ]]; then
|
|
371
|
+
owner_pid="$(cat "$lock_dir/pid" 2>/dev/null || true)"
|
|
372
|
+
fi
|
|
373
|
+
if [[ "$owner_pid" =~ ^[0-9]+$ ]] && ! kill -0 "$owner_pid" 2>/dev/null; then
|
|
374
|
+
automation_warn "removing stale $label lock owned by stopped PID $owner_pid"
|
|
375
|
+
rm -f "$lock_dir/pid" "$lock_dir/acquired-at"
|
|
376
|
+
rmdir "$lock_dir" 2>/dev/null || true
|
|
377
|
+
if mkdir "$lock_dir" 2>/dev/null; then
|
|
378
|
+
return 0
|
|
379
|
+
fi
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
automation_die "$label lock is already held${owner_pid:+ by PID $owner_pid}"
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
automation_acquire_lock() {
|
|
386
|
+
local task_id="$1"
|
|
387
|
+
local lock_dir="$AUTOMATION_LOCKS_DIR/$task_id.state.lock"
|
|
388
|
+
automation_ensure_runtime_layout
|
|
389
|
+
automation_create_lock_dir "$lock_dir" "task state $task_id" || return 1
|
|
390
|
+
printf '%s\n' "$$" > "$lock_dir/pid"
|
|
391
|
+
printf '%s\n' "$(automation_now)" > "$lock_dir/acquired-at"
|
|
392
|
+
AUTOMATION_HELD_LOCK="$lock_dir"
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
automation_release_lock() {
|
|
396
|
+
if [[ -n "${AUTOMATION_HELD_LOCK:-}" && -d "$AUTOMATION_HELD_LOCK" ]]; then
|
|
397
|
+
rm -f "$AUTOMATION_HELD_LOCK/pid" "$AUTOMATION_HELD_LOCK/acquired-at"
|
|
398
|
+
rmdir "$AUTOMATION_HELD_LOCK" 2>/dev/null || true
|
|
399
|
+
fi
|
|
400
|
+
AUTOMATION_HELD_LOCK=""
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
automation_acquire_run_lock() {
|
|
404
|
+
local task_id="$1"
|
|
405
|
+
local lock_dir="$AUTOMATION_LOCKS_DIR/$task_id.run.lock"
|
|
406
|
+
automation_ensure_runtime_layout
|
|
407
|
+
automation_create_lock_dir "$lock_dir" "task orchestration $task_id" || return 1
|
|
408
|
+
printf '%s\n' "$$" > "$lock_dir/pid"
|
|
409
|
+
printf '%s\n' "$(automation_now)" > "$lock_dir/acquired-at"
|
|
410
|
+
AUTOMATION_HELD_RUN_LOCK="$lock_dir"
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
automation_release_run_lock() {
|
|
414
|
+
if [[ -n "${AUTOMATION_HELD_RUN_LOCK:-}" && -d "$AUTOMATION_HELD_RUN_LOCK" ]]; then
|
|
415
|
+
rm -f "$AUTOMATION_HELD_RUN_LOCK/pid" "$AUTOMATION_HELD_RUN_LOCK/acquired-at"
|
|
416
|
+
rmdir "$AUTOMATION_HELD_RUN_LOCK" 2>/dev/null || true
|
|
417
|
+
fi
|
|
418
|
+
AUTOMATION_HELD_RUN_LOCK=""
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
automation_repository_lease_dir() {
|
|
422
|
+
printf '%s/repository.workspace.lease\n' "$AUTOMATION_LOCKS_DIR"
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
automation_assert_repository_lease_available() {
|
|
426
|
+
local task_id="$1"
|
|
427
|
+
local lease_dir lease_file owner
|
|
428
|
+
lease_dir="$(automation_repository_lease_dir)"
|
|
429
|
+
lease_file="$lease_dir/lease.json"
|
|
430
|
+
[[ -d "$lease_dir" ]] || return 0
|
|
431
|
+
[[ -f "$lease_file" ]] || {
|
|
432
|
+
automation_die "repository workspace lease is malformed: $lease_dir"
|
|
433
|
+
return 1
|
|
434
|
+
}
|
|
435
|
+
owner="$(jq -er '.taskId' "$lease_file")" || return 1
|
|
436
|
+
[[ "$owner" == "$task_id" ]] || {
|
|
437
|
+
automation_die "repository workspace is leased by $owner"
|
|
438
|
+
return 1
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
automation_acquire_repository_lease() {
|
|
443
|
+
local task_id="$1"
|
|
444
|
+
local source_root="$2"
|
|
445
|
+
local strategy="$3"
|
|
446
|
+
local lease_dir lease_file tmp
|
|
447
|
+
automation_ensure_runtime_layout
|
|
448
|
+
lease_dir="$(automation_repository_lease_dir)"
|
|
449
|
+
lease_file="$lease_dir/lease.json"
|
|
450
|
+
|
|
451
|
+
if ! mkdir "$lease_dir" 2>/dev/null; then
|
|
452
|
+
automation_assert_repository_lease_available "$task_id" || return 1
|
|
453
|
+
jq -e \
|
|
454
|
+
--arg taskId "$task_id" \
|
|
455
|
+
--arg sourceRoot "$source_root" \
|
|
456
|
+
--arg strategy "$strategy" \
|
|
457
|
+
'.taskId == $taskId and .sourceRoot == $sourceRoot and .workspaceStrategy == $strategy' \
|
|
458
|
+
"$lease_file" >/dev/null || {
|
|
459
|
+
automation_die "repository workspace lease does not match the current task metadata"
|
|
460
|
+
return 1
|
|
461
|
+
}
|
|
462
|
+
return 0
|
|
463
|
+
fi
|
|
464
|
+
|
|
465
|
+
tmp="$(mktemp "$lease_dir/.lease.XXXXXX")"
|
|
466
|
+
jq -n \
|
|
467
|
+
--arg taskId "$task_id" \
|
|
468
|
+
--arg sourceRoot "$source_root" \
|
|
469
|
+
--arg strategy "$strategy" \
|
|
470
|
+
--arg acquiredAt "$(automation_now)" \
|
|
471
|
+
'{taskId: $taskId, sourceRoot: $sourceRoot,
|
|
472
|
+
workspaceStrategy: $strategy, acquiredAt: $acquiredAt}' > "$tmp"
|
|
473
|
+
mv "$tmp" "$lease_file"
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
automation_require_repository_lease() {
|
|
477
|
+
local task_id="$1"
|
|
478
|
+
local source_root="$2"
|
|
479
|
+
local strategy="$3"
|
|
480
|
+
local lease_file
|
|
481
|
+
lease_file="$(automation_repository_lease_dir)/lease.json"
|
|
482
|
+
[[ -f "$lease_file" ]] || {
|
|
483
|
+
automation_die "repository workspace lease is missing for $task_id"
|
|
484
|
+
return 1
|
|
485
|
+
}
|
|
486
|
+
jq -e \
|
|
487
|
+
--arg taskId "$task_id" \
|
|
488
|
+
--arg sourceRoot "$source_root" \
|
|
489
|
+
--arg strategy "$strategy" \
|
|
490
|
+
'.taskId == $taskId and .sourceRoot == $sourceRoot and .workspaceStrategy == $strategy' \
|
|
491
|
+
"$lease_file" >/dev/null || {
|
|
492
|
+
automation_die "repository workspace lease is owned by another task or workspace"
|
|
493
|
+
return 1
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
automation_release_repository_lease() {
|
|
498
|
+
local task_id="$1"
|
|
499
|
+
local lease_dir lease_file owner
|
|
500
|
+
lease_dir="$(automation_repository_lease_dir)"
|
|
501
|
+
lease_file="$lease_dir/lease.json"
|
|
502
|
+
[[ -d "$lease_dir" ]] || return 0
|
|
503
|
+
[[ -f "$lease_file" ]] || {
|
|
504
|
+
automation_die "repository workspace lease is malformed: $lease_dir"
|
|
505
|
+
return 1
|
|
506
|
+
}
|
|
507
|
+
owner="$(jq -er '.taskId' "$lease_file")" || return 1
|
|
508
|
+
[[ "$owner" == "$task_id" ]] || {
|
|
509
|
+
automation_die "cannot release repository workspace lease owned by $owner"
|
|
510
|
+
return 1
|
|
511
|
+
}
|
|
512
|
+
rm -f "$lease_file"
|
|
513
|
+
rmdir "$lease_dir"
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
automation_transition_allowed() {
|
|
517
|
+
local from="$1"
|
|
518
|
+
local to="$2"
|
|
519
|
+
local actor="$3"
|
|
520
|
+
|
|
521
|
+
case "$from:$to:$actor" in
|
|
522
|
+
CONTRACT_REVIEW:APPROVED_CONTRACT:human) return 0 ;;
|
|
523
|
+
APPROVED_CONTRACT:PREPARING:orchestrator) return 0 ;;
|
|
524
|
+
PREPARING:PENDING:orchestrator) return 0 ;;
|
|
525
|
+
PREPARING:BLOCKED:orchestrator) return 0 ;;
|
|
526
|
+
APPROVED_CONTRACT:PENDING:human) return 0 ;;
|
|
527
|
+
TEST_FAILED:PENDING:human) return 0 ;;
|
|
528
|
+
BLOCKED:PENDING:human) return 0 ;;
|
|
529
|
+
CHANGES_REQUESTED:PENDING:human) return 0 ;;
|
|
530
|
+
PENDING:CODING:coder-launcher) return 0 ;;
|
|
531
|
+
PENDING:BLOCKED:preflight) return 0 ;;
|
|
532
|
+
PENDING:BLOCKED:coder) return 0 ;;
|
|
533
|
+
PENDING:BLOCKED:orchestrator) return 0 ;;
|
|
534
|
+
CODING:BLOCKED:preflight) return 0 ;;
|
|
535
|
+
CODING:BLOCKED:coder) return 0 ;;
|
|
536
|
+
CODING:BLOCKED:orchestrator) return 0 ;;
|
|
537
|
+
CODING:READY_FOR_REVIEW:quality-gate) return 0 ;;
|
|
538
|
+
CODING:TEST_FAILED:quality-gate) return 0 ;;
|
|
539
|
+
CODING:BLOCKED:quality-gate) return 0 ;;
|
|
540
|
+
READY_FOR_REVIEW:REVIEWING:orchestrator) return 0 ;;
|
|
541
|
+
READY_FOR_REVIEW:BLOCKED:orchestrator) return 0 ;;
|
|
542
|
+
REVIEWING:AWAITING_HUMAN:reviewer) return 0 ;;
|
|
543
|
+
REVIEWING:CHANGES_REQUESTED:reviewer) return 0 ;;
|
|
544
|
+
REVIEWING:BLOCKED:orchestrator) return 0 ;;
|
|
545
|
+
BLOCKED:REVIEWING:human) return 0 ;;
|
|
546
|
+
CHANGES_REQUESTED:CODING:orchestrator) return 0 ;;
|
|
547
|
+
CHANGES_REQUESTED:NEEDS_HUMAN:orchestrator) return 0 ;;
|
|
548
|
+
AWAITING_HUMAN:INTEGRATING:integrator) return 0 ;;
|
|
549
|
+
INTEGRATING:COMPLETED:integrator) return 0 ;;
|
|
550
|
+
INTEGRATING:INTEGRATION_BLOCKED:integrator) return 0 ;;
|
|
551
|
+
PREPARING:ABORTED:human|PENDING:ABORTED:human|CODING:ABORTED:human|READY_FOR_REVIEW:ABORTED:human|REVIEWING:ABORTED:human|CHANGES_REQUESTED:ABORTED:human|AWAITING_HUMAN:ABORTED:human|BLOCKED:ABORTED:human|TEST_FAILED:ABORTED:human|NEEDS_HUMAN:ABORTED:human|INTEGRATION_BLOCKED:ABORTED:human) return 0 ;;
|
|
552
|
+
*) return 1 ;;
|
|
553
|
+
esac
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
automation_initialize_state() {
|
|
557
|
+
local task_id="$1"
|
|
558
|
+
local initial_state="$2"
|
|
559
|
+
local actor="$3"
|
|
560
|
+
local note="$4"
|
|
561
|
+
local state_file evidence_dir tmp now
|
|
562
|
+
|
|
563
|
+
automation_ensure_runtime_layout
|
|
564
|
+
state_file="$(automation_state_path "$task_id")"
|
|
565
|
+
[[ ! -e "$state_file" ]] || automation_die "state already exists for $task_id"
|
|
566
|
+
evidence_dir="$(automation_evidence_path "$task_id")"
|
|
567
|
+
mkdir -p "$evidence_dir"
|
|
568
|
+
now="$(automation_now)"
|
|
569
|
+
tmp="$(mktemp "$AUTOMATION_STATE_DIR/.${task_id}.XXXXXX")"
|
|
570
|
+
jq -n \
|
|
571
|
+
--arg taskId "$task_id" \
|
|
572
|
+
--arg state "$initial_state" \
|
|
573
|
+
--arg updatedAt "$now" \
|
|
574
|
+
--arg updatedBy "$actor" \
|
|
575
|
+
--arg note "$note" \
|
|
576
|
+
'{taskId: $taskId, state: $state, revision: 1, updatedAt: $updatedAt, updatedBy: $updatedBy, note: $note}' \
|
|
577
|
+
> "$tmp"
|
|
578
|
+
mv "$tmp" "$state_file"
|
|
579
|
+
jq -nc \
|
|
580
|
+
--arg taskId "$task_id" \
|
|
581
|
+
--arg from "" \
|
|
582
|
+
--arg to "$initial_state" \
|
|
583
|
+
--arg actor "$actor" \
|
|
584
|
+
--arg at "$now" \
|
|
585
|
+
--arg note "$note" \
|
|
586
|
+
'{taskId: $taskId, from: $from, to: $to, actor: $actor, at: $at, note: $note}' \
|
|
587
|
+
>> "$evidence_dir/transitions.jsonl"
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
automation_transition_state() {
|
|
591
|
+
local task_id="$1"
|
|
592
|
+
local expected="$2"
|
|
593
|
+
local next="$3"
|
|
594
|
+
local actor="$4"
|
|
595
|
+
local note="$5"
|
|
596
|
+
local state_file evidence_dir actual revision now tmp
|
|
597
|
+
|
|
598
|
+
automation_acquire_lock "$task_id" || return 1
|
|
599
|
+
state_file="$(automation_state_path "$task_id")"
|
|
600
|
+
evidence_dir="$(automation_evidence_path "$task_id")"
|
|
601
|
+
mkdir -p "$evidence_dir"
|
|
602
|
+
|
|
603
|
+
if [[ ! -f "$state_file" ]]; then
|
|
604
|
+
automation_release_lock
|
|
605
|
+
automation_die "state does not exist for $task_id"
|
|
606
|
+
return 1
|
|
607
|
+
fi
|
|
608
|
+
|
|
609
|
+
actual="$(jq -er '.state' "$state_file")" || {
|
|
610
|
+
automation_release_lock
|
|
611
|
+
automation_die "invalid state file for $task_id"
|
|
612
|
+
return 1
|
|
613
|
+
}
|
|
614
|
+
if [[ "$actual" != "$expected" ]]; then
|
|
615
|
+
automation_release_lock
|
|
616
|
+
automation_die "state mismatch for $task_id: expected $expected, found $actual"
|
|
617
|
+
return 1
|
|
618
|
+
fi
|
|
619
|
+
if ! automation_transition_allowed "$expected" "$next" "$actor"; then
|
|
620
|
+
automation_release_lock
|
|
621
|
+
automation_die "forbidden transition: $expected -> $next by $actor"
|
|
622
|
+
return 1
|
|
623
|
+
fi
|
|
624
|
+
|
|
625
|
+
revision="$(jq -er '.revision' "$state_file")"
|
|
626
|
+
revision=$((revision + 1))
|
|
627
|
+
now="$(automation_now)"
|
|
628
|
+
tmp="$(mktemp "$AUTOMATION_STATE_DIR/.${task_id}.XXXXXX")"
|
|
629
|
+
jq \
|
|
630
|
+
--arg state "$next" \
|
|
631
|
+
--argjson revision "$revision" \
|
|
632
|
+
--arg updatedAt "$now" \
|
|
633
|
+
--arg updatedBy "$actor" \
|
|
634
|
+
--arg note "$note" \
|
|
635
|
+
'.state = $state | .revision = $revision | .updatedAt = $updatedAt | .updatedBy = $updatedBy | .note = $note' \
|
|
636
|
+
"$state_file" > "$tmp"
|
|
637
|
+
mv "$tmp" "$state_file"
|
|
638
|
+
jq -nc \
|
|
639
|
+
--arg taskId "$task_id" \
|
|
640
|
+
--arg from "$expected" \
|
|
641
|
+
--arg to "$next" \
|
|
642
|
+
--arg actor "$actor" \
|
|
643
|
+
--arg at "$now" \
|
|
644
|
+
--arg note "$note" \
|
|
645
|
+
'{taskId: $taskId, from: $from, to: $to, actor: $actor, at: $at, note: $note}' \
|
|
646
|
+
>> "$evidence_dir/transitions.jsonl"
|
|
647
|
+
automation_release_lock
|
|
648
|
+
automation_info "$task_id: $expected -> $next"
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
automation_record_json() {
|
|
652
|
+
local destination="$1"
|
|
653
|
+
local tmp
|
|
654
|
+
mkdir -p "$(dirname "$destination")"
|
|
655
|
+
tmp="$(mktemp "$(dirname "$destination")/.record.XXXXXX")"
|
|
656
|
+
cat > "$tmp"
|
|
657
|
+
mv "$tmp" "$destination"
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
automation_append_json() {
|
|
661
|
+
local destination="$1"
|
|
662
|
+
local value
|
|
663
|
+
mkdir -p "$(dirname "$destination")"
|
|
664
|
+
value="$(cat)"
|
|
665
|
+
jq -ce . <<< "$value" >> "$destination"
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
automation_worktree_base() {
|
|
669
|
+
local configured repo_parent repo_name
|
|
670
|
+
configured="$(jq -r '.worktreeBase // ""' "$AUTOMATION_CONFIG")"
|
|
671
|
+
if [[ -n "${AUTOMATION_WORKTREE_BASE:-}" ]]; then
|
|
672
|
+
if [[ "${AUTOMATION_TEST_MODE:-0}" != "1" ]]; then
|
|
673
|
+
automation_die "AUTOMATION_WORKTREE_BASE is reserved for the test suite"
|
|
674
|
+
return 1
|
|
675
|
+
fi
|
|
676
|
+
configured="$AUTOMATION_WORKTREE_BASE"
|
|
677
|
+
fi
|
|
678
|
+
if [[ -z "$configured" ]]; then
|
|
679
|
+
repo_parent="$(dirname "$AUTOMATION_ROOT")"
|
|
680
|
+
repo_name="$(basename "$AUTOMATION_ROOT")"
|
|
681
|
+
configured="$repo_parent/$repo_name-worktrees"
|
|
682
|
+
elif [[ "$configured" != /* ]]; then
|
|
683
|
+
configured="$AUTOMATION_ROOT/$configured"
|
|
684
|
+
fi
|
|
685
|
+
case "$configured/" in
|
|
686
|
+
"$AUTOMATION_ROOT/"*)
|
|
687
|
+
automation_die "worktree base must be outside the source repository: $configured"
|
|
688
|
+
return 1
|
|
689
|
+
;;
|
|
690
|
+
esac
|
|
691
|
+
printf '%s\n' "$configured"
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
automation_task_branch() {
|
|
695
|
+
local task_id="$1"
|
|
696
|
+
printf 'automation/%s\n' "$(tr '[:upper:]' '[:lower:]' <<< "$task_id")"
|
|
697
|
+
}
|