@profoundlogic/coderflow-server 0.2.1

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 (202) hide show
  1. package/LICENSE.txt +322 -0
  2. package/README.md +158 -0
  3. package/dist/LICENSE.txt +322 -0
  4. package/dist/README.md +158 -0
  5. package/dist/base-image/Dockerfile +184 -0
  6. package/dist/base-image/agent-wrapper.sh +143 -0
  7. package/dist/base-image/apply-local-state.sh +357 -0
  8. package/dist/base-image/coder-git-credential-helper +307 -0
  9. package/dist/base-image/entrypoint.sh +942 -0
  10. package/dist/base-image/ssh_config_template +41 -0
  11. package/dist/base-image/start-code-server.sh +76 -0
  12. package/dist/base-image/sync-repos.sh +170 -0
  13. package/dist/base-image/vscode-extensions.txt +10 -0
  14. package/dist/base-image/vscode-settings.json +41 -0
  15. package/dist/coder-server.js +2 -0
  16. package/dist/config/cli-models.json +45 -0
  17. package/dist/config/imported-skills.schema.json +83 -0
  18. package/dist/config/skill-catalog.json +18 -0
  19. package/dist/config/skill-catalog.schema.json +140 -0
  20. package/dist/config.js +1 -0
  21. package/dist/examples/oidc.json.example +11 -0
  22. package/dist/lib/agent-keepalive.js +1 -0
  23. package/dist/lib/api-keys.js +1 -0
  24. package/dist/lib/apiKeys.js +1 -0
  25. package/dist/lib/auto-judge.js +1 -0
  26. package/dist/lib/basic-auth.js +1 -0
  27. package/dist/lib/build-history.js +1 -0
  28. package/dist/lib/build-output-service.js +1 -0
  29. package/dist/lib/build-scheduler.js +1 -0
  30. package/dist/lib/build-service.js +1 -0
  31. package/dist/lib/claude-oauth-refresh.js +1 -0
  32. package/dist/lib/cli/build.js +1 -0
  33. package/dist/lib/cli/config-command.js +1 -0
  34. package/dist/lib/cli/config.js +1 -0
  35. package/dist/lib/cli/create-user.js +1 -0
  36. package/dist/lib/cli/init.js +1 -0
  37. package/dist/lib/cli/jira.js +1 -0
  38. package/dist/lib/cli/license.js +1 -0
  39. package/dist/lib/cli/server-manager.js +1 -0
  40. package/dist/lib/container-tokens.js +1 -0
  41. package/dist/lib/data-dir.js +1 -0
  42. package/dist/lib/deployment-history.js +1 -0
  43. package/dist/lib/deployment-service.js +1 -0
  44. package/dist/lib/docker-utils.js +1 -0
  45. package/dist/lib/email.js +1 -0
  46. package/dist/lib/emailTemplates.js +1 -0
  47. package/dist/lib/entitlement.js +1 -0
  48. package/dist/lib/fetch-utils.js +1 -0
  49. package/dist/lib/git-provider-service.js +1 -0
  50. package/dist/lib/git-provider-setup/assets/coderflow_github_app.png +0 -0
  51. package/dist/lib/git-provider-setup/github-setup-handler.js +1 -0
  52. package/dist/lib/git-provider-setup/index.js +1 -0
  53. package/dist/lib/git-provider-setup/setup-factory.js +1 -0
  54. package/dist/lib/git-provider-setup/setup-interface.js +1 -0
  55. package/dist/lib/git-providers/azure-devops-provider.js +1 -0
  56. package/dist/lib/git-providers/github-app-provider.js +1 -0
  57. package/dist/lib/git-providers/index.js +1 -0
  58. package/dist/lib/git-providers/provider-factory.js +1 -0
  59. package/dist/lib/git-providers/provider-interface.js +1 -0
  60. package/dist/lib/jira-client.js +1 -0
  61. package/dist/lib/logger.js +1 -0
  62. package/dist/lib/model-fetcher.js +1 -0
  63. package/dist/lib/notifications.js +1 -0
  64. package/dist/lib/oidc-auth.js +1 -0
  65. package/dist/lib/oidc-device-flow.js +1 -0
  66. package/dist/lib/passwordTokens.js +1 -0
  67. package/dist/lib/pin-cascade.js +1 -0
  68. package/dist/lib/provider-accounts.js +1 -0
  69. package/dist/lib/provider-oauth.js +1 -0
  70. package/dist/lib/provider-profile.js +1 -0
  71. package/dist/lib/provider-token-refresh.js +1 -0
  72. package/dist/lib/roles.js +1 -0
  73. package/dist/lib/secrets.js +1 -0
  74. package/dist/lib/state-capture.js +1 -0
  75. package/dist/lib/static-files.js +1 -0
  76. package/dist/lib/task-name-generator.js +1 -0
  77. package/dist/lib/users.js +1 -0
  78. package/dist/middleware/requireAuth.js +1 -0
  79. package/dist/middleware/requireInit.js +1 -0
  80. package/dist/middleware/requirePermission.js +1 -0
  81. package/dist/package-lock.json +4151 -0
  82. package/dist/package.json +50 -0
  83. package/dist/routes/apiKeys.js +1 -0
  84. package/dist/routes/auth-oidc.js +1 -0
  85. package/dist/routes/auth.js +1 -0
  86. package/dist/routes/build.js +1 -0
  87. package/dist/routes/containers.js +1 -0
  88. package/dist/routes/deploy-task.js +1 -0
  89. package/dist/routes/environment-management.js +1 -0
  90. package/dist/routes/environments.js +1 -0
  91. package/dist/routes/external-skills.js +1 -0
  92. package/dist/routes/git-credentials.js +1 -0
  93. package/dist/routes/git-provider-setup.js +1 -0
  94. package/dist/routes/health.js +1 -0
  95. package/dist/routes/jira.js +1 -0
  96. package/dist/routes/objective-management.js +1 -0
  97. package/dist/routes/password.js +1 -0
  98. package/dist/routes/prompt.js +1 -0
  99. package/dist/routes/provider-auth.js +1 -0
  100. package/dist/routes/qa.js +1 -0
  101. package/dist/routes/settings.js +1 -0
  102. package/dist/routes/skill-management.js +1 -0
  103. package/dist/routes/skills.js +1 -0
  104. package/dist/routes/tasks.js +2 -0
  105. package/dist/routes/templates.js +1 -0
  106. package/dist/routes/test-task.js +1 -0
  107. package/dist/routes/test.js +1 -0
  108. package/dist/routes/users.js +1 -0
  109. package/dist/routes/visualizations.js +1 -0
  110. package/dist/schemas/template-metadata.schema.json +178 -0
  111. package/dist/scripts/create-user.js +2 -0
  112. package/dist/shipped-skills/environment-instructions/SKILL.md +154 -0
  113. package/dist/shipped-skills/environment-templates/SKILL.md +282 -0
  114. package/dist/shipped-skills/objective-management/SKILL.md +238 -0
  115. package/dist/shipped-skills/skill-editor/SKILL.md +326 -0
  116. package/dist/start.js +2 -0
  117. package/dist/web-ui/public/activity-detail-modal.js +1 -0
  118. package/dist/web-ui/public/activity-feed.js +1 -0
  119. package/dist/web-ui/public/activity-formatters.js +1 -0
  120. package/dist/web-ui/public/agent-event-parser.js +1 -0
  121. package/dist/web-ui/public/app.js +1 -0
  122. package/dist/web-ui/public/approve-dialog.js +1 -0
  123. package/dist/web-ui/public/coderflow-logo-reversed.svg +46 -0
  124. package/dist/web-ui/public/coderflow-logo.svg +46 -0
  125. package/dist/web-ui/public/comments-widget.js +1 -0
  126. package/dist/web-ui/public/docs/.nojekyll +0 -0
  127. package/dist/web-ui/public/docs/README.md +26 -0
  128. package/dist/web-ui/public/docs/_sidebar.md +47 -0
  129. package/dist/web-ui/public/docs/admin/ai-providers.md +132 -0
  130. package/dist/web-ui/public/docs/admin/email-notifications.md +69 -0
  131. package/dist/web-ui/public/docs/admin/environments.md +215 -0
  132. package/dist/web-ui/public/docs/admin/git-providers.md +147 -0
  133. package/dist/web-ui/public/docs/admin/installation.md +313 -0
  134. package/dist/web-ui/public/docs/admin/skills.md +35 -0
  135. package/dist/web-ui/public/docs/admin/sso.md +241 -0
  136. package/dist/web-ui/public/docs/admin/users-and-roles.md +57 -0
  137. package/dist/web-ui/public/docs/code/cli.md +102 -0
  138. package/dist/web-ui/public/docs/code/files-and-editing.md +86 -0
  139. package/dist/web-ui/public/docs/code/terminal-access.md +110 -0
  140. package/dist/web-ui/public/docs/code/vscode-extension.md +58 -0
  141. package/dist/web-ui/public/docs/getting-started/core-concepts.md +129 -0
  142. package/dist/web-ui/public/docs/getting-started/overview.md +46 -0
  143. package/dist/web-ui/public/docs/index.html +151 -0
  144. package/dist/web-ui/public/docs/integrations/custom.md +58 -0
  145. package/dist/web-ui/public/docs/integrations/ibmi/overview.md +58 -0
  146. package/dist/web-ui/public/docs/integrations/overview.md +48 -0
  147. package/dist/web-ui/public/docs/objectives/qa-mode.md +90 -0
  148. package/dist/web-ui/public/docs/objectives/staged-tasks.md +60 -0
  149. package/dist/web-ui/public/docs/objectives/working-with-objectives.md +102 -0
  150. package/dist/web-ui/public/docs/tasks/approval-and-deployment.md +83 -0
  151. package/dist/web-ui/public/docs/tasks/creating-tasks.md +111 -0
  152. package/dist/web-ui/public/docs/tasks/judging.md +114 -0
  153. package/dist/web-ui/public/docs/tasks/providing-feedback.md +41 -0
  154. package/dist/web-ui/public/docs/tasks/task-groups.md +73 -0
  155. package/dist/web-ui/public/docs/tasks/winner-selection.md +75 -0
  156. package/dist/web-ui/public/docs/templates/batch-processing.md +152 -0
  157. package/dist/web-ui/public/docs/templates/task-templates.md +44 -0
  158. package/dist/web-ui/public/docs/templates/template-examples.md +93 -0
  159. package/dist/web-ui/public/docs/testing/profound-automated-testing.md +77 -0
  160. package/dist/web-ui/public/docs/testing/task-visualizations.md +42 -0
  161. package/dist/web-ui/public/docs/testing/testing-menu.md +118 -0
  162. package/dist/web-ui/public/environments.css +3942 -0
  163. package/dist/web-ui/public/environments.html +1791 -0
  164. package/dist/web-ui/public/environments.js +1 -0
  165. package/dist/web-ui/public/favicon-16.png +0 -0
  166. package/dist/web-ui/public/favicon-32.png +0 -0
  167. package/dist/web-ui/public/favicon.ico +0 -0
  168. package/dist/web-ui/public/feedback-widget.css +3133 -0
  169. package/dist/web-ui/public/feedback-widget.js +1 -0
  170. package/dist/web-ui/public/git-history.css +2663 -0
  171. package/dist/web-ui/public/git-history.html +272 -0
  172. package/dist/web-ui/public/git-history.js +1 -0
  173. package/dist/web-ui/public/git-status.js +1 -0
  174. package/dist/web-ui/public/index.html +1459 -0
  175. package/dist/web-ui/public/index.js +1 -0
  176. package/dist/web-ui/public/login.html +346 -0
  177. package/dist/web-ui/public/login.js +1 -0
  178. package/dist/web-ui/public/markdown-editor.js +1 -0
  179. package/dist/web-ui/public/markdown-file-editor.js +1 -0
  180. package/dist/web-ui/public/modal-maximize.js +1 -0
  181. package/dist/web-ui/public/notifications.js +1 -0
  182. package/dist/web-ui/public/server-health.js +1 -0
  183. package/dist/web-ui/public/settings.css +761 -0
  184. package/dist/web-ui/public/settings.html +1044 -0
  185. package/dist/web-ui/public/settings.js +1 -0
  186. package/dist/web-ui/public/setup-password.html +355 -0
  187. package/dist/web-ui/public/setup-password.js +1 -0
  188. package/dist/web-ui/public/skills.css +1949 -0
  189. package/dist/web-ui/public/skills.html +820 -0
  190. package/dist/web-ui/public/skills.js +1 -0
  191. package/dist/web-ui/public/sse-client.js +1 -0
  192. package/dist/web-ui/public/sse-shared-worker.js +1 -0
  193. package/dist/web-ui/public/styles.css +18614 -0
  194. package/dist/web-ui/public/task.html +1779 -0
  195. package/dist/web-ui/public/task.js +1 -0
  196. package/dist/web-ui/public/terminal.html +45 -0
  197. package/dist/web-ui/public/terminal.js +1 -0
  198. package/dist/web-ui/public/theme.js +1 -0
  199. package/dist/web-ui/public/users.html +298 -0
  200. package/dist/web-ui/public/users.js +1 -0
  201. package/dist/web-ui/public/variant-grouping.js +1 -0
  202. package/package.json +63 -0
@@ -0,0 +1,942 @@
1
+ #!/bin/bash
2
+ # CoderFlow Base Image Entrypoint
3
+ # Handles task execution with logging and exit code management
4
+
5
+ set -o pipefail # Catch errors in pipes
6
+ # Note: We don't use 'set -e' because we want to capture exit codes
7
+
8
+ # Task metadata
9
+ TASK_ID="${TASK_ID:-$(date +%s)-$$}"
10
+ TASK_TYPE="${TASK_TYPE:-manual}"
11
+ ENVIRONMENT="${ENVIRONMENT:-base}"
12
+ TASK_OUTPUT_DIR="${TASK_OUTPUT_DIR:-/task-output}"
13
+ START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
14
+ LOG_FILE="$TASK_OUTPUT_DIR/log.txt"
15
+
16
+ # Ensure output directory and log file exist
17
+ mkdir -p "$TASK_OUTPUT_DIR"
18
+ touch "$LOG_FILE"
19
+
20
+ # Decode parameters and env vars provided by server
21
+ PARAMETERS_JSON='{}'
22
+ if [ -n "$TASK_PARAMETERS_B64" ]; then
23
+ decoded_parameters=$(printf '%s' "$TASK_PARAMETERS_B64" | base64 --decode 2>/dev/null)
24
+ if [ $? -eq 0 ] && [ -n "$decoded_parameters" ]; then
25
+ PARAMETERS_JSON="$decoded_parameters"
26
+ fi
27
+ fi
28
+
29
+ ENV_VARS_JSON='{}'
30
+ if [ -n "$TASK_ENV_VARS_B64" ]; then
31
+ decoded_env=$(printf '%s' "$TASK_ENV_VARS_B64" | base64 --decode 2>/dev/null)
32
+ if [ $? -eq 0 ] && [ -n "$decoded_env" ]; then
33
+ ENV_VARS_JSON="$decoded_env"
34
+ fi
35
+ fi
36
+
37
+ # Logging functions
38
+ log() {
39
+ local message="[$(date +'%Y-%m-%d %H:%M:%S')] $*"
40
+ echo "$message"
41
+ if [ -n "$LOG_FILE" ]; then
42
+ printf '%s\n' "$message" >> "$LOG_FILE"
43
+ fi
44
+ }
45
+
46
+ log_error() {
47
+ log "ERROR: $*"
48
+ }
49
+
50
+ log_success() {
51
+ log "SUCCESS: $*"
52
+ }
53
+
54
+ # Enhanced timing function with millisecond precision (only when DEBUG_TIMING is enabled)
55
+ log_timing() {
56
+ if [ "${DEBUG_TIMING:-false}" = "true" ]; then
57
+ local message="[$(date +'%Y-%m-%d %H:%M:%S.%3N')] [TIMING] $*"
58
+ echo "$message"
59
+ if [ -n "$LOG_FILE" ]; then
60
+ printf '%s\n' "$message" >> "$LOG_FILE"
61
+ fi
62
+ fi
63
+ }
64
+
65
+ # Mark timing checkpoint
66
+ TIMING_START=$(date +%s%3N)
67
+ timing_checkpoint() {
68
+ if [ "${DEBUG_TIMING:-false}" = "true" ]; then
69
+ local label="$1"
70
+ local now=$(date +%s%3N)
71
+ local elapsed=$((now - TIMING_START))
72
+ log_timing "$label (elapsed: ${elapsed}ms since start)"
73
+ fi
74
+ }
75
+
76
+ # Setup git credentials early if available (needed for git operations)
77
+ # Git credential.helper store is pre-configured in the Dockerfile.
78
+ # Server passes credentials via CODER_GIT_CREDS environment variable (JSON array format).
79
+ setup_git_credentials() {
80
+ # Check for git credentials passed via environment variable (JSON array)
81
+ if [ -n "$CODER_GIT_CREDS" ]; then
82
+ # Parse JSON array and write each credential line to file
83
+ echo "$CODER_GIT_CREDS" | jq -r '.[]' > /home/coder/.git-credentials 2>/dev/null
84
+ if [ $? -eq 0 ]; then
85
+ chown coder:coder /home/coder/.git-credentials
86
+ chmod 600 /home/coder/.git-credentials
87
+ log "Static Git credentials configured"
88
+ else
89
+ log "Failed to parse static Git credentials from environment variable"
90
+ fi
91
+ fi
92
+ }
93
+
94
+ # Setup git user identity if provided via environment variables
95
+ # CODER_USER_NAME and CODER_USER_EMAIL are passed from the server based on the authenticated user
96
+ setup_git_user_identity() {
97
+ if [ -n "$CODER_USER_NAME" ] || [ -n "$CODER_USER_EMAIL" ]; then
98
+ if [ -n "$CODER_USER_NAME" ]; then
99
+ git config --file /home/coder/.gitconfig user.name "$CODER_USER_NAME"
100
+ fi
101
+ if [ -n "$CODER_USER_EMAIL" ]; then
102
+ git config --file /home/coder/.gitconfig user.email "$CODER_USER_EMAIL"
103
+ fi
104
+ chown coder:coder /home/coder/.gitconfig
105
+ chmod 644 /home/coder/.gitconfig
106
+ fi
107
+ }
108
+
109
+ # Setup cache directory for dynamic git credential helper
110
+ # This helper fetches fresh tokens from the server on-demand, solving the
111
+ # GitHub App token 1-hour expiration issue for long-running containers.
112
+ # Cache is stored at ~/.cache/coder-git-credentials/ with 700 permissions.
113
+ setup_credential_helper_cache() {
114
+ local cache_dir="/home/coder/.cache/coder-git-credentials"
115
+
116
+ # Create cache directory with secure permissions
117
+ mkdir -p "$cache_dir"
118
+ chmod 700 "$cache_dir"
119
+ chown coder:coder "$cache_dir"
120
+
121
+ # Also ensure parent .cache directory has correct ownership
122
+ chown coder:coder /home/coder/.cache 2>/dev/null || true
123
+ }
124
+
125
+ # Update or clone repositories from configuration
126
+ update_repositories() {
127
+ timing_checkpoint "update_repositories: START"
128
+ local workspace_dir="${WORKSPACE_DIR:-/workspace}"
129
+
130
+ # Create workspace directory
131
+ mkdir -p "$workspace_dir"
132
+
133
+ # Check if REPOS_CONFIG is set
134
+ if [ -z "$REPOS_CONFIG" ]; then
135
+ log "No REPOS_CONFIG provided, skipping repository update" >&2
136
+ timing_checkpoint "update_repositories: END (no config)"
137
+ return 0
138
+ fi
139
+
140
+ # Configure Git to use SSH without strict host key checking
141
+ export GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
142
+
143
+ log "Updating repositories in $workspace_dir..." >&2
144
+ timing_checkpoint "update_repositories: Config loaded"
145
+
146
+ # Parse JSON array using jq
147
+ # Expected format: [{"name":"repo1","url":"https://...","branch":"main"},...]
148
+ local repo_count=$(echo "$REPOS_CONFIG" | jq 'length' 2>/dev/null)
149
+
150
+ if [ -z "$repo_count" ] || [ "$repo_count" -eq 0 ]; then
151
+ log "No repositories configured" >&2
152
+ return 0
153
+ fi
154
+
155
+ log "Found $repo_count repositories" >&2
156
+
157
+ # Feature flag for parallel updates (can be disabled via env var if issues arise)
158
+ # Skip parallel mode if only 1 repo
159
+ local parallel_updates="${PARALLEL_REPO_UPDATES:-true}"
160
+ if [ "$repo_count" -eq 1 ]; then
161
+ parallel_updates="false"
162
+ fi
163
+
164
+ if [ "$parallel_updates" = "true" ]; then
165
+ log "Using parallel repository updates" >&2
166
+ timing_checkpoint "update_repositories: Starting parallel updates"
167
+ else
168
+ log "Using sequential repository updates" >&2
169
+ fi
170
+
171
+ # Array to store background job PIDs
172
+ declare -a update_pids=()
173
+
174
+ # Process each repository using jq
175
+ for index in $(seq 0 $((repo_count - 1))); do
176
+ # Extract repository details using jq, supporting both object and string entries
177
+ local repo_json=$(echo "$REPOS_CONFIG" | jq -c ".[$index]" 2>/dev/null)
178
+
179
+ if [ -z "$repo_json" ] || [ "$repo_json" = "null" ]; then
180
+ log_error "Invalid repository configuration at index $index" >&2
181
+ continue
182
+ fi
183
+
184
+ local repo_name=$(echo "$repo_json" | jq -r 'if type == "object" then (.name // empty) else . end' 2>/dev/null)
185
+ local repo_url=$(echo "$repo_json" | jq -r 'if type == "object" then (.url // empty) else empty end' 2>/dev/null)
186
+ local repo_branch=$(echo "$repo_json" | jq -r 'if type == "object" then (.branch // "main") else "main" end' 2>/dev/null)
187
+ local repo_path_from_json=$(echo "$repo_json" | jq -r 'if type == "object" then (.path // empty) else empty end' 2>/dev/null)
188
+
189
+ if [ -z "$repo_name" ] || [ "$repo_name" = "null" ]; then
190
+ log_error "Invalid repository configuration at index $index" >&2
191
+ continue
192
+ fi
193
+
194
+ if [ -z "$repo_url" ] || [ "$repo_url" = "null" ]; then
195
+ log "Skipping automated update for $repo_name (no repository URL provided)" >&2
196
+ continue
197
+ fi
198
+
199
+ # Use path field from JSON if available, otherwise default to repo_name
200
+ local repo_path
201
+ if [ -n "$repo_path_from_json" ] && [ "$repo_path_from_json" != "null" ]; then
202
+ repo_path="$workspace_dir/$repo_path_from_json"
203
+ else
204
+ repo_path="$workspace_dir/$repo_name"
205
+ fi
206
+
207
+ # Define update function (will be run either inline or in background)
208
+ update_single_repo() {
209
+ local repo_name="$1"
210
+ local repo_url="$2"
211
+ local repo_branch="$3"
212
+ local repo_path="$4"
213
+
214
+ # Check for repo-specific branch override (e.g., PROFOUNDJS_BRANCH)
215
+ local env_var_name=$(echo "$repo_name" | tr '[:lower:]' '[:upper:]' | tr '-' '_')_BRANCH
216
+ local branch_override=$(eval echo \$$env_var_name)
217
+
218
+ # Strip origin/ prefix if present (handles case where UI sends full remote ref)
219
+ if [[ "$branch_override" == origin/* ]]; then
220
+ branch_override="${branch_override#origin/}"
221
+ log "Stripped origin/ prefix from branch override: $branch_override" >&2
222
+ fi
223
+
224
+ # Determine final branch: repo-specific override > CODER_BRANCH > CHECKOUT_BRANCH > config branch
225
+ local branch="${branch_override:-${CODER_BRANCH:-${CHECKOUT_BRANCH:-$repo_branch}}}"
226
+
227
+ # Check if repo already exists (cloned during image build)
228
+ if [ -d "$repo_path/.git" ]; then
229
+ timing_checkpoint "update_repositories: Updating $repo_name START"
230
+ log "Updating $repo_name..." >&2
231
+
232
+ # Log branch override if used
233
+ if [ -n "$branch_override" ]; then
234
+ log "Using branch override $env_var_name=$branch (config: $repo_branch)" >&2
235
+ elif [ "$branch" != "$repo_branch" ]; then
236
+ log "Using branch override: $branch (config: $repo_branch)" >&2
237
+ fi
238
+
239
+ # Run git operations as coder user - output to both stdout and log file
240
+ log "Running git operations for $repo_name..."
241
+
242
+ # Check if we're running as root or coder user
243
+ if [ "$(id -u)" -eq 0 ]; then
244
+ # Running as root, use runuser to switch to coder
245
+ GIT_SSH_COMMAND="$GIT_SSH_COMMAND" runuser -u coder -- bash -c "
246
+ cd '$repo_path' || exit 1
247
+
248
+ # Temporarily disable post-checkout hook during startup
249
+ if [ -f '.git/hooks/post-checkout' ] && [ -x '.git/hooks/post-checkout' ]; then
250
+ mv '.git/hooks/post-checkout' '.git/hooks/post-checkout.disabled' 2>/dev/null || true
251
+ fi
252
+
253
+ # Optimize git performance for startup operations
254
+ git config --local fetch.prune true 2>/dev/null || true
255
+ git config --local diff.autoRefreshIndex false 2>/dev/null || true
256
+
257
+ # Fetch latest refs from remote (required for new branches not known locally)
258
+ git fetch --depth=50 origin '$branch' 2>&1 || true
259
+
260
+ # Checkout branch
261
+ git checkout '$branch' 2>&1 || exit \$?
262
+
263
+ # Reset package-lock.json to avoid merge conflicts from npm version differences
264
+ git checkout -- package-lock.json '**/package-lock.json' 2>/dev/null || true
265
+
266
+ # Pull latest changes
267
+ git pull --depth=50 --no-stat 2>&1
268
+ pull_exit=\$?
269
+
270
+ # Re-enable post-checkout hook
271
+ if [ -f '.git/hooks/post-checkout.disabled' ]; then
272
+ mv '.git/hooks/post-checkout.disabled' '.git/hooks/post-checkout' 2>/dev/null || true
273
+ fi
274
+
275
+ exit \$pull_exit
276
+ " 2>&1 | while IFS= read -r line; do
277
+ # Filter out routine messages, keep important ones
278
+ case "$line" in
279
+ *"Already on"*|*"Your branch is up to date"*|*"Already up to date"*|*"Warning: Permanently added"*)
280
+ # Skip routine messages
281
+ ;;
282
+ *)
283
+ # Show important messages (errors, updates, etc.)
284
+ echo " $line"
285
+ [ -n "$LOG_FILE" ] && echo " $line" >> "$LOG_FILE"
286
+ ;;
287
+ esac
288
+ done
289
+ local update_exit=${PIPESTATUS[0]}
290
+ else
291
+ # Already running as coder, execute directly
292
+ (
293
+ cd "$repo_path" || exit 1
294
+
295
+ # Temporarily disable post-checkout hook during startup
296
+ if [ -f '.git/hooks/post-checkout' ] && [ -x '.git/hooks/post-checkout' ]; then
297
+ mv '.git/hooks/post-checkout' '.git/hooks/post-checkout.disabled' 2>/dev/null || true
298
+ fi
299
+
300
+ # Optimize git performance for startup operations
301
+ git config --local fetch.prune true 2>/dev/null || true
302
+ git config --local diff.autoRefreshIndex false 2>/dev/null || true
303
+
304
+ # Fetch latest refs from remote (required for new branches not known locally)
305
+ git fetch --depth=50 origin "$branch" 2>&1 || true
306
+
307
+ # Checkout branch
308
+ git checkout "$branch" 2>&1 || exit $?
309
+
310
+ # Reset package-lock.json to avoid merge conflicts from npm version differences
311
+ git checkout -- package-lock.json '**/package-lock.json' 2>/dev/null || true
312
+
313
+ # Pull latest changes
314
+ git pull --depth=50 --no-stat 2>&1
315
+ pull_exit=$?
316
+
317
+ # Re-enable post-checkout hook
318
+ if [ -f '.git/hooks/post-checkout.disabled' ]; then
319
+ mv '.git/hooks/post-checkout.disabled' '.git/hooks/post-checkout' 2>/dev/null || true
320
+ fi
321
+
322
+ exit $pull_exit
323
+ ) 2>&1 | while IFS= read -r line; do
324
+ # Filter out routine messages, keep important ones
325
+ case "$line" in
326
+ *"Already on"*|*"Your branch is up to date"*|*"Already up to date"*|*"Warning: Permanently added"*)
327
+ # Skip routine messages
328
+ ;;
329
+ *)
330
+ # Show important messages (errors, updates, etc.)
331
+ echo " $line"
332
+ [ -n "$LOG_FILE" ] && echo " $line" >> "$LOG_FILE"
333
+ ;;
334
+ esac
335
+ done
336
+ local update_exit=${PIPESTATUS[0]}
337
+ fi
338
+
339
+ if [ $update_exit -eq 0 ]; then
340
+ timing_checkpoint "update_repositories: $repo_name git pull SUCCESS"
341
+ else
342
+ timing_checkpoint "update_repositories: $repo_name git pull FAILED"
343
+ fi
344
+
345
+ timing_checkpoint "update_repositories: Updating $repo_name END"
346
+ return $update_exit
347
+ else
348
+ # Fallback: clone if not present (shouldn't happen in proper setup)
349
+ log "Cloning $repo_name from $repo_url (branch: $branch)..." >&2
350
+
351
+ # Ensure parent directory exists
352
+ mkdir -p "$(dirname "$repo_path")"
353
+
354
+ # Run clone as coder user with environment preserved
355
+ if [ "$(id -u)" -eq 0 ]; then
356
+ # Running as root, use runuser
357
+ GIT_SSH_COMMAND="$GIT_SSH_COMMAND" runuser -u coder -- bash -c "
358
+ if git clone --depth=50 --no-single-branch --quiet --branch '$branch' '$repo_url' '$repo_path' 2>&1; then
359
+ echo 'Successfully cloned $repo_name' >&2
360
+ exit 0
361
+ else
362
+ echo 'ERROR: Failed to clone $repo_name from $repo_url' >&2
363
+ exit 1
364
+ fi
365
+ "
366
+ else
367
+ # Already running as coder
368
+ if git clone --depth=50 --no-single-branch --quiet --branch "$branch" "$repo_url" "$repo_path" 2>&1; then
369
+ echo "Successfully cloned $repo_name" >&2
370
+ exit 0
371
+ else
372
+ echo "ERROR: Failed to clone $repo_name from $repo_url" >&2
373
+ exit 1
374
+ fi
375
+ fi
376
+ return $?
377
+ fi
378
+ }
379
+
380
+ # Run update either in background or inline
381
+ if [ "$parallel_updates" = "true" ]; then
382
+ # Launch in background and track PID
383
+ update_single_repo "$repo_name" "$repo_url" "$repo_branch" "$repo_path" &
384
+ update_pids+=($!)
385
+ else
386
+ # Run immediately (sequential)
387
+ update_single_repo "$repo_name" "$repo_url" "$repo_branch" "$repo_path"
388
+ fi
389
+ done
390
+
391
+ # Wait for all background jobs if using parallel mode
392
+ if [ "$parallel_updates" = "true" ]; then
393
+ timing_checkpoint "update_repositories: Waiting for parallel updates"
394
+ log "Waiting for ${#update_pids[@]} repository updates to complete..." >&2
395
+
396
+ local failed_count=0
397
+ for pid in "${update_pids[@]}"; do
398
+ if ! wait $pid; then
399
+ failed_count=$((failed_count + 1))
400
+ fi
401
+ done
402
+
403
+ if [ $failed_count -gt 0 ]; then
404
+ log_error "$failed_count repository update(s) failed" >&2
405
+ fi
406
+ fi
407
+
408
+ log "Repository update complete" >&2
409
+ timing_checkpoint "update_repositories: END (all repos complete)"
410
+ return 0
411
+ }
412
+
413
+ # Execute environment setup script if provided
414
+ run_setup_script() {
415
+ if [ -z "$SETUP_SCRIPT" ]; then
416
+ log "No setup script specified" >&2
417
+ return 0
418
+ fi
419
+
420
+ if [ ! -f "$SETUP_SCRIPT" ]; then
421
+ log_error "Setup script not found: $SETUP_SCRIPT" >&2
422
+ return 1
423
+ fi
424
+
425
+ log "Executing setup script: $SETUP_SCRIPT" >&2
426
+
427
+ # Run setup script as coder user with .bashrc sourced
428
+ if [ "$(id -u)" -eq 0 ]; then
429
+ # Running as root, use runuser
430
+ local setup_output=$(runuser -u coder -- bash -c "
431
+ # Source .bashrc if it exists to get user environment (for env vars like NODE_PATH, etc.)
432
+ [ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null || true
433
+ # Execute the setup script
434
+ bash '$SETUP_SCRIPT'
435
+ " 2>&1)
436
+ local setup_exit=$?
437
+ else
438
+ # Already running as coder
439
+ local setup_output=$(bash -c "
440
+ # Source .bashrc if it exists to get user environment (for env vars like NODE_PATH, etc.)
441
+ [ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null || true
442
+ # Execute the setup script
443
+ bash '$SETUP_SCRIPT'
444
+ " 2>&1)
445
+ local setup_exit=$?
446
+ fi
447
+
448
+ # Output the captured setup script output to stderr for logging
449
+ if [ -n "$setup_output" ]; then
450
+ echo "$setup_output" >&2
451
+ fi
452
+
453
+ if [ $setup_exit -eq 0 ]; then
454
+ log "Setup script completed successfully" >&2
455
+ return 0
456
+ else
457
+ log_error "Setup script failed with exit code $setup_exit" >&2
458
+ return 1
459
+ fi
460
+ }
461
+
462
+ # Generate git patches from changed repositories
463
+ # Optimized version: parallel processing, uses git diff --stat for faster stats
464
+ generate_patches() {
465
+ local workspace_dir="${WORKSPACE_DIR:-/workspace}"
466
+ local patches_dir="$TASK_OUTPUT_DIR/patches"
467
+ local repos_changed_json="[]"
468
+
469
+ log "Checking for repository changes in $workspace_dir..." >&2
470
+
471
+ # Create patches directory and temp directory for parallel results
472
+ mkdir -p "$patches_dir"
473
+ local temp_dir=$(mktemp -d)
474
+ trap "rm -rf $temp_dir" RETURN
475
+
476
+ # Check if workspace directory exists
477
+ if [ ! -d "$workspace_dir" ]; then
478
+ log "No workspace directory found at $workspace_dir" >&2
479
+ echo "$repos_changed_json"
480
+ return
481
+ fi
482
+
483
+ # Helper function to process a single repository (runs in subshell for parallel execution)
484
+ # Writes JSON result to a temp file instead of stdout
485
+ _process_repo_parallel() {
486
+ local repo_name="$1"
487
+ local repo_path="$2"
488
+ local output_file="$3"
489
+ local patches_dir="$4"
490
+ local ignore_lockfile="${IGNORE_LOCKFILE_CHANGES:-false}"
491
+
492
+ cd "$repo_path" || return 1
493
+
494
+ # Quick check: any changes at all? (single git status call)
495
+ local status_output=$(git status --porcelain 2>/dev/null)
496
+ if [ -z "$status_output" ]; then
497
+ log "No changes in $repo_name" >&2
498
+ return 1
499
+ fi
500
+
501
+ log "Changes detected in $repo_name, generating patch..." >&2
502
+
503
+ # Stage all changes
504
+ git add -A
505
+
506
+ # Generate patch with optional lockfile exclusion
507
+ local patch_file="$repo_name.patch"
508
+ if [ "$ignore_lockfile" = "true" ]; then
509
+ git diff --cached -- ':!package-lock.json' ':!**/package-lock.json' > "$patches_dir/$patch_file"
510
+ else
511
+ git diff --cached > "$patches_dir/$patch_file"
512
+ fi
513
+
514
+ # Check if patch has content
515
+ if [ ! -s "$patches_dir/$patch_file" ]; then
516
+ log "No changes to patch for $repo_name" >&2
517
+ rm -f "$patches_dir/$patch_file"
518
+ return 1
519
+ fi
520
+
521
+ # Use git diff --stat for fast stats
522
+ local stat_output
523
+ if [ "$ignore_lockfile" = "true" ]; then
524
+ stat_output=$(git diff --cached --stat -- ':!package-lock.json' ':!**/package-lock.json' 2>/dev/null | tail -1)
525
+ else
526
+ stat_output=$(git diff --cached --stat 2>/dev/null | tail -1)
527
+ fi
528
+
529
+ # Parse stat output: "X files changed, Y insertions(+), Z deletions(-)"
530
+ local files_changed=0
531
+ local lines_added=0
532
+ local lines_deleted=0
533
+
534
+ if [ -n "$stat_output" ]; then
535
+ files_changed=$(echo "$stat_output" | grep -oE '[0-9]+ file' | grep -oE '[0-9]+' || echo "0")
536
+ lines_added=$(echo "$stat_output" | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0")
537
+ lines_deleted=$(echo "$stat_output" | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0")
538
+ fi
539
+
540
+ # Ensure we have valid numbers
541
+ files_changed=${files_changed:-0}
542
+ lines_added=${lines_added:-0}
543
+ lines_deleted=${lines_deleted:-0}
544
+
545
+ # Get list of changed files using git diff --name-only
546
+ local files_list
547
+ if [ "$ignore_lockfile" = "true" ]; then
548
+ files_list=$(git diff --cached --name-only -- ':!package-lock.json' ':!**/package-lock.json' 2>/dev/null)
549
+ else
550
+ files_list=$(git diff --cached --name-only 2>/dev/null)
551
+ fi
552
+ local files_detail=$(echo "$files_list" | jq -R -s -c 'split("\n") | map(select(length > 0)) | map({path: ., added: 0, deleted: 0})')
553
+
554
+ # Fallback if jq fails
555
+ if [ -z "$files_detail" ] || [ "$files_detail" = "null" ]; then
556
+ files_detail='[]'
557
+ fi
558
+
559
+ log "Patch generated: $patch_file ($files_changed files, +$lines_added -$lines_deleted)" >&2
560
+
561
+ # Write JSON to output file (for parallel collection)
562
+ cat > "$output_file" <<REPO_EOF
563
+ {
564
+ "name": "$repo_name",
565
+ "path": "$repo_path",
566
+ "files_changed": $files_changed,
567
+ "lines_added": $lines_added,
568
+ "lines_deleted": $lines_deleted,
569
+ "patch_file": "$patch_file",
570
+ "files": $files_detail
571
+ }
572
+ REPO_EOF
573
+ return 0
574
+ }
575
+
576
+ # Export function and variables for subshells
577
+ export -f _process_repo_parallel
578
+ export -f log
579
+ export IGNORE_LOCKFILE_CHANGES
580
+
581
+ local pids=()
582
+ local repo_index=0
583
+
584
+ # If REPOS_CONFIG is set, use it to find repositories (respects path property)
585
+ if [ -n "$REPOS_CONFIG" ]; then
586
+ local config_repo_count=$(echo "$REPOS_CONFIG" | jq 'length' 2>/dev/null)
587
+
588
+ if [ -n "$config_repo_count" ] && [ "$config_repo_count" -gt 0 ]; then
589
+ log "Using REPOS_CONFIG to check $config_repo_count repositories (parallel)" >&2
590
+
591
+ for index in $(seq 0 $((config_repo_count - 1))); do
592
+ local repo_json=$(echo "$REPOS_CONFIG" | jq -c ".[$index]" 2>/dev/null)
593
+
594
+ if [ -z "$repo_json" ] || [ "$repo_json" = "null" ]; then
595
+ continue
596
+ fi
597
+
598
+ local repo_name=$(echo "$repo_json" | jq -r 'if type == "object" then (.name // empty) else . end' 2>/dev/null)
599
+ local repo_path_from_json=$(echo "$repo_json" | jq -r 'if type == "object" then (.path // empty) else empty end' 2>/dev/null)
600
+
601
+ if [ -z "$repo_name" ] || [ "$repo_name" = "null" ]; then
602
+ continue
603
+ fi
604
+
605
+ # Use path field from JSON if available, otherwise default to repo_name
606
+ local repo_path
607
+ if [ -n "$repo_path_from_json" ] && [ "$repo_path_from_json" != "null" ]; then
608
+ repo_path="$workspace_dir/$repo_path_from_json"
609
+ else
610
+ repo_path="$workspace_dir/$repo_name"
611
+ fi
612
+
613
+ # Check if this is a git repository
614
+ if [ ! -d "$repo_path/.git" ]; then
615
+ log "Skipping $repo_name (not a git repository at $repo_path)" >&2
616
+ continue
617
+ fi
618
+
619
+ # Launch repo processing in background
620
+ local output_file="$temp_dir/repo_$repo_index.json"
621
+ (
622
+ _process_repo_parallel "$repo_name" "$repo_path" "$output_file" "$patches_dir"
623
+ ) &
624
+ pids+=($!)
625
+ repo_index=$((repo_index + 1))
626
+ done
627
+ fi
628
+ else
629
+ # Fallback: no REPOS_CONFIG, scan top-level directories
630
+ log "No REPOS_CONFIG found, scanning top-level directories (parallel)" >&2
631
+ for repo_path in "$workspace_dir"/*; do
632
+ if [ -d "$repo_path/.git" ]; then
633
+ local repo_name=$(basename "$repo_path")
634
+
635
+ # Launch repo processing in background
636
+ local output_file="$temp_dir/repo_$repo_index.json"
637
+ (
638
+ _process_repo_parallel "$repo_name" "$repo_path" "$output_file" "$patches_dir"
639
+ ) &
640
+ pids+=($!)
641
+ repo_index=$((repo_index + 1))
642
+ fi
643
+ done
644
+ fi
645
+
646
+ # Wait for all background jobs to complete
647
+ for pid in "${pids[@]}"; do
648
+ wait $pid 2>/dev/null || true
649
+ done
650
+
651
+ # Collect results from temp files into JSON array
652
+ local repo_count=0
653
+ for result_file in "$temp_dir"/repo_*.json; do
654
+ if [ -f "$result_file" ] && [ -s "$result_file" ]; then
655
+ local repo_result=$(cat "$result_file")
656
+ if [ -n "$repo_result" ]; then
657
+ if [ "$repo_count" -eq 0 ]; then
658
+ repos_changed_json="[$repo_result"
659
+ else
660
+ repos_changed_json="$repos_changed_json,$repo_result"
661
+ fi
662
+ repo_count=$((repo_count + 1))
663
+ fi
664
+ fi
665
+ done
666
+
667
+ # Close JSON array
668
+ if [ "$repo_count" -gt 0 ]; then
669
+ repos_changed_json="$repos_changed_json]"
670
+ fi
671
+
672
+ log "Patch generation complete: $repo_count repositories with changes" >&2
673
+ echo "$repos_changed_json"
674
+ }
675
+
676
+ # Generate task.json metadata file
677
+ generate_task_json() {
678
+ local exit_code=$1
679
+ shift
680
+ local end_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
681
+ local status="completed"
682
+
683
+ if [ $exit_code -ne 0 ]; then
684
+ status="failed"
685
+ fi
686
+
687
+ # Source environment variables set by setup script (e.g., IGNORE_LOCKFILE_CHANGES)
688
+ if [ -f ~/.bash_env ]; then
689
+ source ~/.bash_env
690
+ fi
691
+
692
+ # Generate patches and get repos_changed JSON
693
+ local repos_changed=$(generate_patches)
694
+
695
+ # Read commit message if available
696
+ local commit_message=""
697
+ local commit_message_file="$TASK_OUTPUT_DIR/commit-message.txt"
698
+ if [ -f "$commit_message_file" ] && [ -s "$commit_message_file" ]; then
699
+ commit_message=$(cat "$commit_message_file")
700
+ # Properly escape commit message for JSON
701
+ commit_message="${commit_message//\\/\\\\}" # Escape backslashes first
702
+ commit_message="${commit_message//\"/\\\"}" # Escape quotes
703
+ commit_message="${commit_message//$'\n'/\\n}" # Escape newlines
704
+ commit_message="${commit_message//$'\r'/\\r}" # Escape carriage returns
705
+ commit_message="${commit_message//$'\t'/\\t}" # Escape tabs
706
+ fi
707
+
708
+ # Properly escape command for JSON
709
+ local escaped_command="$*"
710
+ escaped_command="${escaped_command//\\/\\\\}" # Escape backslashes first
711
+ escaped_command="${escaped_command//\"/\\\"}" # Escape quotes
712
+ escaped_command="${escaped_command//$'\n'/\\n}" # Escape newlines
713
+ escaped_command="${escaped_command//$'\r'/\\r}" # Escape carriage returns
714
+ escaped_command="${escaped_command//$'\t'/\\t}" # Escape tabs
715
+
716
+ # Create task.json in the output directory
717
+ cat > "$TASK_OUTPUT_DIR/task.json" <<EOF
718
+ {
719
+ "task_id": "$TASK_ID",
720
+ "task_type": "$TASK_TYPE",
721
+ "environment": "$ENVIRONMENT",
722
+ "status": "$status",
723
+ "started_at": "$START_TIME",
724
+ "completed_at": "$end_time",
725
+ "exit_code": $exit_code,
726
+ "container_id": "$(hostname)",
727
+ "command": "$escaped_command",
728
+ "parameters": $PARAMETERS_JSON,
729
+ "env_vars": $ENV_VARS_JSON,
730
+ "summary_path": "summary.md",
731
+ "log_path": "log.txt",
732
+ "instructions_path": "task-instructions.md",
733
+ "commit_message": "$commit_message",
734
+ "repos_changed": $repos_changed
735
+ }
736
+ EOF
737
+
738
+ log "Task metadata written to $TASK_OUTPUT_DIR/task.json"
739
+ }
740
+
741
+ # Main execution
742
+ main() {
743
+ timing_checkpoint "main: START"
744
+ log "CoderFlow task execution starting..."
745
+ log "Working directory: $(pwd)"
746
+
747
+ setup_git_credentials
748
+ setup_git_user_identity
749
+ setup_credential_helper_cache
750
+
751
+ # Log once if skipping all initialization (for follow-up tasks)
752
+ if [ "$SKIP_INIT" = "true" ]; then
753
+ log "Skipping initialization: repository updates, local state, AGENTS.md, setup script (SKIP_INIT=true)"
754
+ fi
755
+
756
+ timing_checkpoint "main: Before repository updates"
757
+ # Update repositories before task execution (pull if exists, clone if not)
758
+ # Skip if SKIP_INIT is set (e.g., for follow-up tasks where initialization is already done)
759
+ # Always update repos first, even with local state (get latest from remote, then apply local changes on top)
760
+ # For interactive containers WITHOUT local state, run in background to avoid blocking terminal startup
761
+ # For task containers OR when local state will be applied, run synchronously
762
+ if [ "$SKIP_INIT" = "true" ]; then
763
+ timing_checkpoint "main: Repository updates skipped"
764
+ elif [ "$CONTAINER_MODE" = "interactive" ] && [ ! -f "/task-output/local-state.json" ]; then
765
+ log "Starting repository updates in background..."
766
+ update_repositories &
767
+ REPO_UPDATE_PID=$!
768
+ timing_checkpoint "main: Repository updates backgrounded (PID: $REPO_UPDATE_PID)"
769
+ else
770
+ update_repositories
771
+ timing_checkpoint "main: Repository updates complete (synchronous)"
772
+ fi
773
+
774
+ timing_checkpoint "main: Before local state application"
775
+ # Apply local repository state if provided (branch setup, staged/unstaged changes, untracked files)
776
+ # Skip if SKIP_INIT is set
777
+ # Local state is applied ON TOP of the latest remote state (git pull already completed synchronously above)
778
+ if [ "$SKIP_INIT" = "true" ]; then
779
+ timing_checkpoint "main: Local state application skipped"
780
+ elif [ -f "/task-output/local-state.json" ]; then
781
+ log "Applying local repository state..."
782
+ # Run as coder user to avoid permission issues with git files
783
+ if [ "$(id -u)" -eq 0 ]; then
784
+ # Running as root, use runuser
785
+ if runuser -u coder bash /usr/local/bin/apply-local-state.sh; then
786
+ log "Local state applied successfully" >&2
787
+ timing_checkpoint "main: Local state application complete"
788
+ else
789
+ log_error "Failed to apply local state (non-fatal, continuing...)" >&2
790
+ timing_checkpoint "main: Local state application failed"
791
+ fi
792
+ else
793
+ # Already running as coder
794
+ if bash /usr/local/bin/apply-local-state.sh; then
795
+ log "Local state applied successfully" >&2
796
+ timing_checkpoint "main: Local state application complete"
797
+ else
798
+ log_error "Failed to apply local state (non-fatal, continuing...)" >&2
799
+ timing_checkpoint "main: Local state application failed"
800
+ fi
801
+ fi
802
+
803
+ # Create marker to signal local state is complete (startup script will wait for this)
804
+ touch /tmp/.local-state-ready 2>/dev/null || true
805
+ log "Local state ready marker created"
806
+ else
807
+ timing_checkpoint "main: No local state file found"
808
+ fi
809
+
810
+ # Copy AGENTS.md to workspace if available (centralized for all environments)
811
+ # This provides AI agents with environment-specific instructions
812
+ # NOTE: Only AGENTS.md is copied - README.md stays in the environment directory
813
+ # README.md is for human users/admins, not for AI agents
814
+ # Skip if SKIP_INIT is set (e.g., for follow-up tasks where AGENTS.md is already present)
815
+ if [ "$SKIP_INIT" = "true" ]; then
816
+ : # Skip - already logged above
817
+ elif [ -n "$ENVIRONMENT" ] && [ -f "/coder-setup/$ENVIRONMENT/AGENTS.md" ]; then
818
+ log "Copying AGENTS.md from /coder-setup/$ENVIRONMENT/" >&2
819
+ cp "/coder-setup/$ENVIRONMENT/AGENTS.md" /workspace/AGENTS.md
820
+
821
+ # Create agent-specific symlink based on CODER_AGENT or default_agent
822
+ AGENT="${CODER_AGENT:-${default_agent:-claude}}"
823
+ if [ "$AGENT" = "claude" ]; then
824
+ ln -sf /workspace/AGENTS.md /workspace/CLAUDE.md
825
+ log "AGENTS.md copied and CLAUDE.md symlink created" >&2
826
+ elif [ "$AGENT" = "gemini" ]; then
827
+ ln -sf /workspace/AGENTS.md /workspace/GEMINI.md
828
+ log "AGENTS.md copied and GEMINI.md symlink created" >&2
829
+ elif [ "$AGENT" = "codex" ]; then
830
+ # Codex doesn't use agent-specific files, just AGENTS.md
831
+ log "AGENTS.md copied (no agent-specific symlink for Codex)" >&2
832
+ else
833
+ log "AGENTS.md copied (unknown agent: $AGENT)" >&2
834
+ fi
835
+ else
836
+ log "No AGENTS.md found for environment: $ENVIRONMENT" >&2
837
+ fi
838
+
839
+ timing_checkpoint "main: Before setup script"
840
+ # Run environment-specific setup script when available
841
+ # Skip if SKIP_INIT is set (e.g., for follow-up tasks where environment is already configured)
842
+ if [ "$SKIP_INIT" = "true" ]; then
843
+ timing_checkpoint "main: Setup script skipped"
844
+ elif ! run_setup_script; then
845
+ log_error "Environment setup script failed; aborting task execution"
846
+ generate_task_json 1 "setup_script"
847
+ return 1
848
+ else
849
+ timing_checkpoint "main: Setup script complete"
850
+ fi
851
+
852
+ local exit_code=0
853
+
854
+ # Update status to indicate initialization is complete
855
+ echo "{\"progress\": 0.01, \"phase\": \"Ready\", \"updated_at\": \"$(date -Iseconds)\"}" > "$TASK_OUTPUT_DIR/.status"
856
+ timing_checkpoint "main: Status file written, ready for task execution"
857
+
858
+ # If SETUP_ONLY mode, return here without executing tasks (for interactive containers)
859
+ if [ "${SETUP_ONLY:-false}" = "true" ]; then
860
+ log "Setup complete (SETUP_ONLY mode), skipping task execution"
861
+ return 0
862
+ fi
863
+
864
+ # If STAGED_TASK mode, complete setup but skip agent execution (user will start later)
865
+ if [ "${STAGED_TASK:-false}" = "true" ]; then
866
+ echo "{\"progress\": 0.01, \"phase\": \"Staged\", \"updated_at\": \"$(date -Iseconds)\"}" > "$TASK_OUTPUT_DIR/.status"
867
+ log "Setup complete (STAGED_TASK mode), waiting for instructions"
868
+ return 0
869
+ fi
870
+
871
+ # If a command was passed, execute it
872
+ # Otherwise, execute default task
873
+ if [ $# -gt 0 ]; then
874
+ timing_checkpoint "main: Before task execution"
875
+ log "Executing task..."
876
+
877
+ {
878
+ # Source ~/.bashrc to load environment variables written by setup.sh
879
+ # This is needed for non-interactive shells (tasks) where .bashrc isn't auto-sourced
880
+ if [ -f ~/.bashrc ]; then
881
+ source ~/.bashrc
882
+ fi
883
+ "$@"
884
+ } 2>&1 | tee -a "$LOG_FILE"
885
+ exit_code=${PIPESTATUS[0]}
886
+
887
+ if [ $exit_code -eq 0 ]; then
888
+ log_success "Task completed successfully (exit code: $exit_code)"
889
+ else
890
+ log_error "Task failed (exit code: $exit_code)"
891
+ fi
892
+ else
893
+ # Default task: simple hello world
894
+ log "No command specified, executing default task..."
895
+
896
+ {
897
+ echo "Hello from CoderFlow!"
898
+ echo "Node.js version: $(node --version)"
899
+ echo "Git version: $(git --version)"
900
+ echo "Current directory: $(pwd)"
901
+ echo "Task completed successfully!"
902
+ } 2>&1 | tee -a "$LOG_FILE"
903
+
904
+ exit_code=0
905
+ log_success "Default task completed (exit code: $exit_code)"
906
+ fi
907
+
908
+ log "Task execution finished"
909
+
910
+ local summary_file="$TASK_OUTPUT_DIR/summary.md"
911
+
912
+ if [ ! -s "$summary_file" ]; then
913
+ if [ $exit_code -eq 0 ]; then
914
+ cat <<SUMMARY_EOF > "$summary_file"
915
+ Task ${TASK_ID} completed successfully.
916
+
917
+ - Task Type: ${TASK_TYPE}
918
+ - Environment: ${ENVIRONMENT}
919
+ SUMMARY_EOF
920
+ else
921
+ cat <<SUMMARY_EOF > "$summary_file"
922
+ Task ${TASK_ID} failed with exit code ${exit_code}.
923
+
924
+ - Task Type: ${TASK_TYPE}
925
+ - Environment: ${ENVIRONMENT}
926
+ SUMMARY_EOF
927
+ fi
928
+ log "Summary file created at $summary_file"
929
+ fi
930
+
931
+ # Generate task metadata
932
+ generate_task_json $exit_code "$@"
933
+
934
+ timing_checkpoint "main: END"
935
+ return $exit_code
936
+ }
937
+
938
+ # Run main and preserve exit code (unless SKIP_MAIN_EXECUTION is set)
939
+ if [ "${SKIP_MAIN_EXECUTION:-false}" != "true" ]; then
940
+ main "$@"
941
+ exit $?
942
+ fi