@jenga-ai/agent 1.0.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 (177) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +340 -0
  3. package/agents/ai_engineer.md +113 -0
  4. package/agents/developer.md +236 -0
  5. package/agents/scrum-master.md +349 -0
  6. package/agents/scrutiny-agent.md +137 -0
  7. package/agents/solution-assessor.md +185 -0
  8. package/agents/tester.md +339 -0
  9. package/bin/jenga.js +70 -0
  10. package/hooks/copilot_session_end.sh +29 -0
  11. package/hooks/on_session_end.sh +238 -0
  12. package/hooks/prompt_router.sh +11 -0
  13. package/hooks/prompt_router_helper.js +52 -0
  14. package/hooks/session_end_helper.js +29 -0
  15. package/hooks/session_end_watcher.sh +24 -0
  16. package/lib/commands/attach.js +47 -0
  17. package/lib/commands/init.js +207 -0
  18. package/lib/commands/start.js +16 -0
  19. package/lib/commands/status.js +53 -0
  20. package/lib/config-schema.js +72 -0
  21. package/lib/inject-settings.js +61 -0
  22. package/lib/mirror.js +244 -0
  23. package/lib/resolve-project-dir.sh +47 -0
  24. package/mcp/execute-ticket/index.js +10 -0
  25. package/mcp/execute-ticket/package.json +5 -0
  26. package/mcp/help/index.js +79 -0
  27. package/mcp/help/package.json +14 -0
  28. package/mcp/router/README.md +19 -0
  29. package/mcp/router/embedder.js +23 -0
  30. package/mcp/router/index.js +204 -0
  31. package/mcp/router/matcher.js +87 -0
  32. package/mcp/router/package-lock.json +1048 -0
  33. package/mcp/router/package.json +11 -0
  34. package/mcp/router/skill-index.js +104 -0
  35. package/package.json +47 -0
  36. package/scripts/board_resolver.sh +46 -0
  37. package/scripts/e25_s01_extract_board_graph.py +292 -0
  38. package/scripts/e25_s01_generate_synthetic_board.py +90 -0
  39. package/scripts/measurement-10x.json +50 -0
  40. package/scripts/measurement-10x.txt +4 -0
  41. package/scripts/measurement-real.json +50 -0
  42. package/scripts/measurement-real.txt +4 -0
  43. package/scripts/postinstall.js +165 -0
  44. package/scripts/todo_cleanup.sh +22 -0
  45. package/scripts/todo_manager.sh +86 -0
  46. package/scripts/validate-board.sh +190 -0
  47. package/scripts/validate-story-format.sh +53 -0
  48. package/skills/brainstorm/SKILL.md +47 -0
  49. package/skills/btw/SKILL.md +42 -0
  50. package/skills/commit/SKILL.md +29 -0
  51. package/skills/commit/assets/user_instructions_template.md +22 -0
  52. package/skills/continue/SKILL.md +29 -0
  53. package/skills/convert/SKILL.md +124 -0
  54. package/skills/convert/convert_cli.py +235 -0
  55. package/skills/convert/tests/sample.csv +4 -0
  56. package/skills/convert/tests/sample.json +5 -0
  57. package/skills/convert/tests/sample.jsonl +3 -0
  58. package/skills/convert/tests/sample.yaml +18 -0
  59. package/skills/convert/tests/sample_obj.csv +2 -0
  60. package/skills/convert/tests/sample_obj.json +9 -0
  61. package/skills/deep-dive/SKILL.md +167 -0
  62. package/skills/do/SKILL.md +88 -0
  63. package/skills/do/assets/sender_template.json +12 -0
  64. package/skills/doc/SKILL.md +314 -0
  65. package/skills/doc/assets/path-objectives.yaml +38 -0
  66. package/skills/doc-sync/SKILL.md +167 -0
  67. package/skills/doc-sync/assets/default_excludes.txt +21 -0
  68. package/skills/doc-sync/assets/doc_targets.md +14 -0
  69. package/skills/dooo/SKILL.md +60 -0
  70. package/skills/error/SKILL.md +29 -0
  71. package/skills/evaluate/SKILL.md +45 -0
  72. package/skills/evaluate/assets/evaluation_invokation_template.yml +3 -0
  73. package/skills/evaluate/assets/evaluation_rapport_template.md +24 -0
  74. package/skills/examplify/SKILL.md +42 -0
  75. package/skills/help/SKILL.md +36 -0
  76. package/skills/improve/SKILL.md +55 -0
  77. package/skills/index/scripts/board-index +4 -0
  78. package/skills/index/scripts/board_index.py +615 -0
  79. package/skills/index/scripts/smoke_test.sh +86 -0
  80. package/skills/init/SKILL.md +44 -0
  81. package/skills/init/assets/.gitignore_template +15 -0
  82. package/skills/init/assets/PROJECT_SUMMARY_template.md +13 -0
  83. package/skills/init/assets/directory_structure.txt +13 -0
  84. package/skills/init/assets/test-config_template.json +4 -0
  85. package/skills/init/assets/workflow_template.json +30 -0
  86. package/skills/init/scripts/init.sh +48 -0
  87. package/skills/jbp/SKILL.md +25 -0
  88. package/skills/jenga/SKILL.md +68 -0
  89. package/skills/lgtm/SKILL.md +21 -0
  90. package/skills/mirror-public/SKILL.md +237 -0
  91. package/skills/mirror-public/assets/config.json +5 -0
  92. package/skills/mirror-public/scripts/mirror.sh +374 -0
  93. package/skills/pi-plan/SKILL.md +62 -0
  94. package/skills/pi-plan/assets/epic.json +7 -0
  95. package/skills/pi-plan/assets/story_template.md +18 -0
  96. package/skills/proceed/SKILL.md +29 -0
  97. package/skills/publish/SKILL.md +351 -0
  98. package/skills/publish/adapters/droplet.md +200 -0
  99. package/skills/publish/adapters/mobile-ios.md +114 -0
  100. package/skills/publish/adapters/npm-ci.md +223 -0
  101. package/skills/publish/adapters/npm.md +121 -0
  102. package/skills/publish/assets/ExportOptions.plist.template +19 -0
  103. package/skills/publish/assets/ci-contract.md +111 -0
  104. package/skills/publish/assets/ownership-matrix.md +17 -0
  105. package/skills/publish/assets/publish.example.json +85 -0
  106. package/skills/publish/assets/publish.example.npm-ci.json +40 -0
  107. package/skills/publish/assets/publish.example.npm.json +41 -0
  108. package/skills/publish/assets/secrets-guide.md +104 -0
  109. package/skills/publish/schemas/fixtures/npm-ci-minimal.json +17 -0
  110. package/skills/publish/schemas/fixtures/npm-ci-with-empty-secrets.json +18 -0
  111. package/skills/publish/schemas/fixtures/npm-ci-with-workflow-path.json +18 -0
  112. package/skills/publish/schemas/publish.schema.json +428 -0
  113. package/skills/publish/scripts/check_target_config.sh +96 -0
  114. package/skills/publish/scripts/droplet_pipeline.sh +208 -0
  115. package/skills/publish/scripts/generate_release_notes.sh +200 -0
  116. package/skills/publish/scripts/ios_pipeline.sh +486 -0
  117. package/skills/publish/scripts/npm_ci_pipeline.sh +225 -0
  118. package/skills/publish/scripts/npm_pipeline.sh +249 -0
  119. package/skills/publish/scripts/publish_common.sh +253 -0
  120. package/skills/publish/scripts/publish_deploy.sh +538 -0
  121. package/skills/publish/scripts/reconcile_tags.sh +135 -0
  122. package/skills/publish/scripts/run_gates.sh +616 -0
  123. package/skills/publish/scripts/setup_wizard.sh +394 -0
  124. package/skills/publish/scripts/show_history.sh +95 -0
  125. package/skills/publish/scripts/suggest_semver_bump.sh +105 -0
  126. package/skills/publish/scripts/validate_config.sh +163 -0
  127. package/skills/publish/scripts/validate_droplet_env.sh +45 -0
  128. package/skills/publish/scripts/validate_ios_env.sh +68 -0
  129. package/skills/publish/scripts/validate_npm_ci_env.sh +71 -0
  130. package/skills/publish/scripts/validate_npm_env.sh +22 -0
  131. package/skills/publish/scripts/write_ledger_entry.sh +126 -0
  132. package/skills/publish/wizards/droplet.md +275 -0
  133. package/skills/publish/wizards/mobile-ios.md +157 -0
  134. package/skills/publish/wizards/npm-ci.md +240 -0
  135. package/skills/publish/wizards/npm.md +224 -0
  136. package/skills/reconcile/SKILL.md +93 -0
  137. package/skills/reconcile/assets/report_format.md +44 -0
  138. package/skills/reconcile-origin/SKILL.md +75 -0
  139. package/skills/reconcile-origin/scripts/reconcile-origin.sh +372 -0
  140. package/skills/redo/SKILL.md +70 -0
  141. package/skills/route/SKILL.md +180 -0
  142. package/skills/self-sync/SKILL.md +73 -0
  143. package/skills/self-sync/scripts/run.js +136 -0
  144. package/skills/skillify/SKILL.md +68 -0
  145. package/skills/skillify/assets/init-new/SKILL.md +35 -0
  146. package/skills/skillify/assets/init-new/assets/.gitignore_template +15 -0
  147. package/skills/skillify/assets/init-new/assets/PROJECT_SUMMARY_template.md +13 -0
  148. package/skills/skillify/assets/init-new/assets/directory_structure.txt +10 -0
  149. package/skills/skillify/assets/init-new/assets/test-config_template.json +4 -0
  150. package/skills/skillify/assets/init-new/assets/workflow_template.json +17 -0
  151. package/skills/skillify/assets/init-new/scripts/init.sh +48 -0
  152. package/skills/skillify/assets/init-old/SKILL.md +124 -0
  153. package/skills/spinoff/SKILL.md +48 -0
  154. package/skills/status/SKILL.md +33 -0
  155. package/skills/status/assets/output_format.md +41 -0
  156. package/skills/todo/SKILL.md +46 -0
  157. package/skills/todo/assets/todo_handoff_template.md +22 -0
  158. package/skills/todo/assets/todo_template.md +3 -0
  159. package/skills/train/SKILL.md +116 -0
  160. package/skills/train/assets/dashboard-templates/classifiers.html +106 -0
  161. package/skills/train/assets/dashboard-templates/nlp.html +102 -0
  162. package/skills/train/assets/dashboard-templates/transformers.html +98 -0
  163. package/skills/train/assets/results-parsers/__init__.py +9 -0
  164. package/skills/train/assets/results-parsers/classifiers.py +84 -0
  165. package/skills/train/assets/results-parsers/nlp.py +88 -0
  166. package/skills/train/assets/results-parsers/reporter.py +154 -0
  167. package/skills/train/assets/results-parsers/transformers.py +120 -0
  168. package/skills/train/train_cli.py +786 -0
  169. package/templates/EXECUTION_PLAN_TEMPLATE.md +43 -0
  170. package/templates/EXECUTION_SUMMARY_TEMPLATE.md +50 -0
  171. package/templates/JENGA_CONFIG_TEMPLATE.json +23 -0
  172. package/templates/PROBLEM_RAPPORT_TEMPLATE.md +88 -0
  173. package/templates/SCRUM_BOARD_SCHEMA.md +311 -0
  174. package/templates/SKILL.md +16 -0
  175. package/templates/SKILL_TEMPLATE.md +28 -0
  176. package/templates/USER_INSTRUCTIONS_TEMPLATE.md +22 -0
  177. package/templates/copilot-instructions.md.tpl +55 -0
@@ -0,0 +1,616 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # /publish quality gate runner
5
+ #
6
+ # Invocation:
7
+ # run_gates.sh <phase> <target> <publish_json_path> [--non-interactive]
8
+ #
9
+ # Arguments:
10
+ # <phase> pre | post
11
+ # <target> target name from publish.json
12
+ # <publish_json_path> path to publish.json
13
+ # --non-interactive disable prompts; any gate failure aborts immediately
14
+ #
15
+ # Exit codes:
16
+ # 0 all required gates passed (or were intentionally skipped with warnings)
17
+ # 2 one or more gates failed
18
+
19
+ usage() {
20
+ cat <<'USAGE'
21
+ Usage: run_gates.sh <phase> <target> <publish_json_path> [--non-interactive]
22
+ USAGE
23
+ }
24
+
25
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ if REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)"; then
27
+ :
28
+ else
29
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
30
+ fi
31
+ PUBLISH_HISTORY="$REPO_ROOT/project/logs/publish-history.json"
32
+ CUSTOM_SCRIPT_PATTERN='^[./a-zA-Z0-9_-]+$'
33
+
34
+ if [[ $# -lt 3 || $# -gt 4 ]]; then
35
+ usage >&2
36
+ exit 1
37
+ fi
38
+
39
+ PHASE="$1"
40
+ TARGET_NAME="$2"
41
+ PUBLISH_JSON="$3"
42
+ NON_INTERACTIVE=0
43
+ PROMPT_INPUT_FD=0
44
+
45
+ if [[ $# -eq 4 ]]; then
46
+ if [[ "$4" != "--non-interactive" ]]; then
47
+ usage >&2
48
+ exit 1
49
+ fi
50
+ NON_INTERACTIVE=1
51
+ fi
52
+
53
+ if [[ ! -t 0 ]]; then
54
+ NON_INTERACTIVE=1
55
+ fi
56
+
57
+ if (( NON_INTERACTIVE == 0 )); then
58
+ if exec 3</dev/tty 2>/dev/null; then
59
+ PROMPT_INPUT_FD=3
60
+ else
61
+ NON_INTERACTIVE=1
62
+ fi
63
+ fi
64
+
65
+ if [[ "$PHASE" != "pre" && "$PHASE" != "post" ]]; then
66
+ echo "Error: <phase> must be 'pre' or 'post'." >&2
67
+ exit 1
68
+ fi
69
+
70
+ if [[ ! -f "$PUBLISH_JSON" ]]; then
71
+ echo "Error: publish config not found at '$PUBLISH_JSON'." >&2
72
+ exit 1
73
+ fi
74
+
75
+ if ! command -v jq >/dev/null 2>&1; then
76
+ echo "Error: jq is required to run publish quality gates." >&2
77
+ exit 1
78
+ fi
79
+
80
+ log_info() {
81
+ printf '→ %s\n' "$1"
82
+ }
83
+
84
+ log_warn() {
85
+ printf '⚠ %s\n' "$1" >&2
86
+ }
87
+
88
+ log_error() {
89
+ printf '✗ %s\n' "$1" >&2
90
+ }
91
+
92
+ ensure_publish_history() {
93
+ mkdir -p "$(dirname "$PUBLISH_HISTORY")"
94
+ if [[ ! -f "$PUBLISH_HISTORY" ]]; then
95
+ echo '[]' > "$PUBLISH_HISTORY"
96
+ fi
97
+ }
98
+
99
+ append_gate_failure_history() {
100
+ local gate_name="$1"
101
+ local entry
102
+ local history_tmp
103
+
104
+ ensure_publish_history
105
+ entry="$(jq -cn \
106
+ --arg gate "$gate_name" \
107
+ --arg phase "$PHASE" \
108
+ --arg target "$TARGET_NAME" \
109
+ --arg date "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
110
+ '{event:"gate_failure",gate:$gate,phase:$phase,target:$target,date:$date,exit_code:2}')"
111
+
112
+ history_tmp="$PUBLISH_HISTORY.tmp"
113
+ jq --argjson entry "$entry" '. += [$entry]' "$PUBLISH_HISTORY" > "$history_tmp"
114
+ mv "$history_tmp" "$PUBLISH_HISTORY"
115
+ }
116
+
117
+ print_results_summary() {
118
+ local result
119
+
120
+ if [[ ${#RESULTS[@]} -eq 0 ]]; then
121
+ return 0
122
+ fi
123
+
124
+ echo "Gate results:"
125
+ for result in "${RESULTS[@]}"; do
126
+ printf ' - %s\n' "$result"
127
+ done
128
+ }
129
+
130
+ jq_target_def() {
131
+ cat <<'JQ'
132
+ def target_obj:
133
+ if (.targets? | type) == "object" then
134
+ .targets[$target]
135
+ elif (.targets? | type) == "array" then
136
+ (.targets[]? | select((.name // .id // .target // "") == $target))
137
+ else
138
+ empty
139
+ end;
140
+ JQ
141
+ }
142
+
143
+ require_target() {
144
+ local exists
145
+ exists="$(jq -r --arg target "$TARGET_NAME" "$(jq_target_def) target_obj | if . == null or . == false then \"\" else \"yes\" end" "$PUBLISH_JSON")"
146
+ if [[ -z "$exists" ]]; then
147
+ log_error "Target '$TARGET_NAME' was not found in '$PUBLISH_JSON'."
148
+ exit 1
149
+ fi
150
+ }
151
+
152
+ TARGET_TYPE=""
153
+
154
+ resolve_target_type() {
155
+ TARGET_TYPE="$(jq -r --arg target "$TARGET_NAME" "$(jq_target_def) target_obj | .type // empty" "$PUBLISH_JSON" | head -n 1)"
156
+ # Default to mobile-ios when unset to preserve legacy behaviour.
157
+ [[ -n "$TARGET_TYPE" ]] || TARGET_TYPE="mobile-ios"
158
+ }
159
+
160
+ get_gate_entries() {
161
+ jq -c --arg target "$TARGET_NAME" --arg phase "$PHASE" "$(jq_target_def)
162
+ target_obj as \$target_config |
163
+ (\$target_config.checks[\$phase] // [])[]? |
164
+ if type == \"string\" then
165
+ {name: ., config: {}}
166
+ elif type == \"object\" then
167
+ if has(\"name\") then {name: .name, config: .}
168
+ elif has(\"gate\") then {name: .gate, config: .}
169
+ elif has(\"type\") then {name: .type, config: .}
170
+ else empty end
171
+ else
172
+ empty
173
+ end
174
+ " "$PUBLISH_JSON"
175
+ }
176
+
177
+ get_value_from_config() {
178
+ local key="$1"
179
+ jq -r --arg target "$TARGET_NAME" --arg key "$key" "$(jq_target_def)
180
+ target_obj as \$target_config |
181
+ (
182
+ \$target_config.checks[\$key] //
183
+ \$target_config[\$key] //
184
+ .checks[\$key] //
185
+ .[\$key] //
186
+ empty
187
+ )
188
+ " "$PUBLISH_JSON"
189
+ }
190
+
191
+ shell_quote() {
192
+ printf '%q' "$1"
193
+ }
194
+
195
+ find_npm_script_command() {
196
+ local script_name="$1"
197
+ local package_file
198
+ while IFS= read -r package_file; do
199
+ if jq -e --arg script_name "$script_name" '.scripts[$script_name] // empty' "$package_file" >/dev/null 2>&1; then
200
+ local package_dir
201
+ package_dir="$(dirname "$package_file")"
202
+ if [[ "$package_dir" == "$REPO_ROOT" ]]; then
203
+ printf 'npm run %s\n' "$script_name"
204
+ else
205
+ printf 'npm --prefix %s run %s\n' "$(shell_quote "$package_dir")" "$script_name"
206
+ fi
207
+ return 0
208
+ fi
209
+ done < <(find "$REPO_ROOT" -name package.json -not -path '*/node_modules/*' -not -path '*/.git/*' | sort)
210
+ return 1
211
+ }
212
+
213
+ # Resolve an npm script from the top-level package.json only (mirrors
214
+ # npm_pipeline.sh, which publishes $REPO_ROOT/package.json). Prints the
215
+ # command form npm uses for the built-in `test` script vs. `npm run <name>`.
216
+ find_npm_script_in_root() {
217
+ local script_name="$1"
218
+ local package_file="$REPO_ROOT/package.json"
219
+
220
+ [[ -f "$package_file" ]] || return 1
221
+ jq -e --arg script_name "$script_name" '.scripts[$script_name] // empty' "$package_file" >/dev/null 2>&1 || return 1
222
+
223
+ # `npm test` is a first-class alias; everything else uses `npm run <name>`.
224
+ if [[ "$script_name" == "test" ]]; then
225
+ printf 'npm test\n'
226
+ else
227
+ printf 'npm run %s\n' "$script_name"
228
+ fi
229
+ return 0
230
+ }
231
+
232
+ find_xcodebuild_command() {
233
+ local action="$1"
234
+ if find "$REPO_ROOT" -maxdepth 4 \( -name '*.xcodeproj' -o -name '*.xcworkspace' \) | grep -q .; then
235
+ printf 'xcodebuild %s\n' "$action"
236
+ return 0
237
+ fi
238
+ return 1
239
+ }
240
+
241
+ resolve_default_command() {
242
+ local gate_name="$1"
243
+
244
+ # NPM targets: resolve strictly against $REPO_ROOT/package.json and never
245
+ # fall through to iOS tooling. Missing scripts are announced with a clear
246
+ # informational note (on stderr so it does not pollute the captured
247
+ # command) before the gate is reported as skipped upstream.
248
+ if [[ "$TARGET_TYPE" == "npm" ]]; then
249
+ case "$gate_name" in
250
+ build|test|lint)
251
+ if find_npm_script_in_root "$gate_name"; then
252
+ return 0
253
+ fi
254
+ printf '→ npm gate %q skipped: no %q script defined in package.json\n' \
255
+ "$gate_name" "$gate_name" >&2
256
+ return 0
257
+ ;;
258
+ *)
259
+ return 0
260
+ ;;
261
+ esac
262
+ fi
263
+
264
+ case "$gate_name" in
265
+ build)
266
+ find_npm_script_command build || find_xcodebuild_command build || true
267
+ ;;
268
+ test)
269
+ find_npm_script_command test || find_xcodebuild_command test || true
270
+ ;;
271
+ lint)
272
+ find_npm_script_command lint || true
273
+ ;;
274
+ type-check)
275
+ find_npm_script_command typecheck || find_npm_script_command type-check || true
276
+ ;;
277
+ smoke-test)
278
+ find_npm_script_command smoke-test || true
279
+ ;;
280
+ *)
281
+ true
282
+ ;;
283
+ esac
284
+ }
285
+
286
+ RUN_OUTPUT=""
287
+ RUN_STATUS=0
288
+ LAST_GATE_COMMAND=""
289
+ RESOLVED_GATE_COMMAND=""
290
+
291
+ run_command_capture() {
292
+ local command="$1"
293
+ LAST_GATE_COMMAND="$command"
294
+ set +e
295
+ RUN_OUTPUT="$(bash -lc "$command" 2>&1)"
296
+ RUN_STATUS=$?
297
+ set -e
298
+ }
299
+
300
+ print_failure_block() {
301
+ local gate_name="$1"
302
+ echo ""
303
+ echo "========== GATE FAILURE ==========" >&2
304
+ echo "Gate: $gate_name" >&2
305
+ echo "Phase: $PHASE" >&2
306
+ echo "Target: $TARGET_NAME" >&2
307
+ echo "Command: ${LAST_GATE_COMMAND:-n/a}" >&2
308
+ echo "Exit code: $RUN_STATUS" >&2
309
+ echo "----------- output -----------" >&2
310
+ if [[ -n "$RUN_OUTPUT" ]]; then
311
+ printf '%s\n' "$RUN_OUTPUT" >&2
312
+ else
313
+ echo "(no output captured)" >&2
314
+ fi
315
+ echo "==================================" >&2
316
+ echo "" >&2
317
+ }
318
+
319
+ attempt_gate() {
320
+ local gate_json="$1"
321
+ local gate_name="$2"
322
+ local resolve_status
323
+
324
+ if resolve_gate_command "$gate_name" "$gate_json"; then
325
+ resolve_status=0
326
+ else
327
+ resolve_status=$?
328
+ fi
329
+ if [[ $resolve_status -ne 0 ]]; then
330
+ return "$resolve_status"
331
+ fi
332
+
333
+ if [[ -z "$RESOLVED_GATE_COMMAND" ]]; then
334
+ log_warn "No command resolved for gate '$gate_name'; skipping."
335
+ return 1
336
+ fi
337
+
338
+ log_info "Running gate '$gate_name'..."
339
+ run_command_capture "$RESOLVED_GATE_COMMAND"
340
+
341
+ if [[ $RUN_STATUS -ne 0 ]]; then
342
+ return 2
343
+ fi
344
+
345
+ return 0
346
+ }
347
+
348
+ handle_gate_failure() {
349
+ local gate_name="$1"
350
+ local gate_json="$2"
351
+ local retry_count=0
352
+ local choice
353
+ local choice_lower
354
+ local retry_status
355
+
356
+ append_gate_failure_history "$gate_name"
357
+ print_failure_block "$gate_name"
358
+
359
+ if (( NON_INTERACTIVE )); then
360
+ return 2
361
+ fi
362
+
363
+ while true; do
364
+ if (( retry_count >= 3 )); then
365
+ log_error "Retry limit reached for gate '$gate_name'. Aborting deploy."
366
+ return 2
367
+ fi
368
+
369
+ read -r -u "$PROMPT_INPUT_FD" -p "[r] Retry after fixing / [a] Abort deploy: " choice || choice="a"
370
+ choice_lower="$(printf '%s' "$choice" | tr '[:upper:]' '[:lower:]')"
371
+ case "$choice_lower" in
372
+ r)
373
+ retry_count=$((retry_count + 1))
374
+ log_info "Retrying gate '$gate_name' (attempt $retry_count of 3)..."
375
+ if attempt_gate "$gate_json" "$gate_name"; then
376
+ retry_status=0
377
+ else
378
+ retry_status=$?
379
+ fi
380
+ case $retry_status in
381
+ 0)
382
+ return 0
383
+ ;;
384
+ 1)
385
+ return 1
386
+ ;;
387
+ 2)
388
+ append_gate_failure_history "$gate_name"
389
+ print_failure_block "$gate_name"
390
+ ;;
391
+ esac
392
+ ;;
393
+ a)
394
+ log_error "Deploy aborted after gate '$gate_name' failed."
395
+ return 2
396
+ ;;
397
+ *)
398
+ echo "Please enter 'r' or 'a'." >&2
399
+ ;;
400
+ esac
401
+ done
402
+ }
403
+
404
+ resolve_custom_script_command() {
405
+ local gate_json="$1"
406
+ local script_path
407
+ local resolved_path
408
+ script_path="$(printf '%s' "$gate_json" | jq -r '.config.path // .config.script // .config.command // .config.custom_script_path // empty')"
409
+ if [[ -z "$script_path" ]]; then
410
+ script_path="$(get_value_from_config custom_script)"
411
+ fi
412
+ if [[ -z "$script_path" ]]; then
413
+ script_path="$(get_value_from_config custom_script_path)"
414
+ fi
415
+ if [[ -z "$script_path" ]]; then
416
+ script_path="$(get_value_from_config custom_script_command)"
417
+ fi
418
+
419
+ if [[ -z "$script_path" ]]; then
420
+ log_warn "No custom script path configured for target '$TARGET_NAME'; skipping custom-script gate."
421
+ return 1
422
+ fi
423
+
424
+ if [[ ! "$script_path" =~ $CUSTOM_SCRIPT_PATTERN ]]; then
425
+ RUN_OUTPUT="Rejected custom-script path '$script_path': only [./a-zA-Z0-9_-] characters are allowed."
426
+ RUN_STATUS=2
427
+ LAST_GATE_COMMAND="$script_path"
428
+ return 2
429
+ fi
430
+
431
+ if [[ "$script_path" = /* ]]; then
432
+ resolved_path="$script_path"
433
+ else
434
+ resolved_path="$REPO_ROOT/${script_path#./}"
435
+ fi
436
+
437
+ if [[ ! -f "$resolved_path" ]]; then
438
+ RUN_OUTPUT="Custom script '$resolved_path' does not exist."
439
+ RUN_STATUS=2
440
+ LAST_GATE_COMMAND="$resolved_path"
441
+ return 2
442
+ fi
443
+
444
+ if [[ -x "$resolved_path" ]]; then
445
+ RESOLVED_GATE_COMMAND="$(shell_quote "$resolved_path")"
446
+ else
447
+ RESOLVED_GATE_COMMAND="bash $(shell_quote "$resolved_path")"
448
+ fi
449
+ return 0
450
+ }
451
+
452
+ resolve_ping_command() {
453
+ local gate_json="$1"
454
+ local ping_url
455
+ ping_url="$(printf '%s' "$gate_json" | jq -r '.config.url // .config.ping_url // empty')"
456
+ if [[ -z "$ping_url" ]]; then
457
+ ping_url="$(get_value_from_config ping_url)"
458
+ fi
459
+
460
+ if [[ -z "$ping_url" ]]; then
461
+ log_warn "No ping URL configured for target '$TARGET_NAME'; skipping ping gate."
462
+ return 1
463
+ fi
464
+
465
+ RESOLVED_GATE_COMMAND="curl -fsS -o /dev/null $(shell_quote "$ping_url")"
466
+ return 0
467
+ }
468
+
469
+ resolve_gate_command() {
470
+ local gate_name="$1"
471
+ local gate_json="$2"
472
+ local command_override=""
473
+ RESOLVED_GATE_COMMAND=""
474
+
475
+ case "$gate_name" in
476
+ build)
477
+ command_override="$(get_value_from_config build_command)"
478
+ ;;
479
+ test)
480
+ command_override="$(get_value_from_config test_command)"
481
+ ;;
482
+ lint)
483
+ command_override="$(get_value_from_config lint_command)"
484
+ ;;
485
+ type-check)
486
+ command_override="$(get_value_from_config typecheck_command)"
487
+ ;;
488
+ smoke-test)
489
+ command_override="$(get_value_from_config smoke_test_command)"
490
+ ;;
491
+ custom-script)
492
+ resolve_custom_script_command "$gate_json"
493
+ return $?
494
+ ;;
495
+ ping)
496
+ resolve_ping_command "$gate_json"
497
+ return $?
498
+ ;;
499
+ *)
500
+ log_warn "Unsupported gate '$gate_name'; skipping."
501
+ return 1
502
+ ;;
503
+ esac
504
+
505
+ if [[ -n "$command_override" ]]; then
506
+ RESOLVED_GATE_COMMAND="$command_override"
507
+ return 0
508
+ fi
509
+
510
+ RESOLVED_GATE_COMMAND="$(resolve_default_command "$gate_name")"
511
+ return 0
512
+ }
513
+
514
+ RESULTS=()
515
+
516
+ run_gate() {
517
+ local gate_json="$1"
518
+ local gate_name
519
+ local gate_status
520
+
521
+ gate_name="$(printf '%s' "$gate_json" | jq -r '.name')"
522
+ if attempt_gate "$gate_json" "$gate_name"; then
523
+ gate_status=0
524
+ else
525
+ gate_status=$?
526
+ fi
527
+ case $gate_status in
528
+ 0)
529
+ RESULTS+=("$gate_name:passed")
530
+ log_info "Gate '$gate_name' passed."
531
+ return 0
532
+ ;;
533
+ 1)
534
+ RESULTS+=("$gate_name:skipped")
535
+ return 0
536
+ ;;
537
+ 2)
538
+ if handle_gate_failure "$gate_name" "$gate_json"; then
539
+ gate_status=0
540
+ else
541
+ gate_status=$?
542
+ fi
543
+ case $gate_status in
544
+ 0)
545
+ RESULTS+=("$gate_name:passed")
546
+ log_info "Gate '$gate_name' passed."
547
+ return 0
548
+ ;;
549
+ 1)
550
+ RESULTS+=("$gate_name:skipped")
551
+ return 0
552
+ ;;
553
+ 2)
554
+ return 2
555
+ ;;
556
+ esac
557
+ ;;
558
+ esac
559
+ }
560
+
561
+ build_gate_list() {
562
+ local gate_json
563
+ local -a mandatory_pre_gates=()
564
+
565
+ if [[ "$PHASE" == "pre" ]]; then
566
+ if [[ "$TARGET_TYPE" == "npm" ]]; then
567
+ # npm-specific mandatory gates: test, build, lint (per E26_S06_T03).
568
+ mandatory_pre_gates=(test build lint)
569
+ else
570
+ mandatory_pre_gates=(build test)
571
+ fi
572
+ local gate
573
+ for gate in "${mandatory_pre_gates[@]}"; do
574
+ printf '{"name":"%s","config":{},"source":"mandatory"}\n' "$gate"
575
+ done
576
+ fi
577
+
578
+ while IFS= read -r gate_json; do
579
+ [[ -z "$gate_json" ]] && continue
580
+ local gate_name
581
+ gate_name="$(printf '%s' "$gate_json" | jq -r '.name')"
582
+ if [[ "$PHASE" == "pre" ]]; then
583
+ local is_mandatory=0
584
+ local m_gate
585
+ for m_gate in "${mandatory_pre_gates[@]}"; do
586
+ if [[ "$gate_name" == "$m_gate" ]]; then
587
+ is_mandatory=1
588
+ break
589
+ fi
590
+ done
591
+ if (( is_mandatory )); then
592
+ log_warn "Ignoring configurable '$gate_name' gate for target '$TARGET_NAME'; mandatory global gates already enforce it."
593
+ continue
594
+ fi
595
+ fi
596
+ printf '%s\n' "$gate_json"
597
+ done < <(get_gate_entries)
598
+ }
599
+
600
+ main() {
601
+ local gate_json
602
+
603
+ require_target
604
+ resolve_target_type
605
+ ensure_publish_history
606
+
607
+ while IFS= read -r gate_json; do
608
+ [[ -z "$gate_json" ]] && continue
609
+ run_gate "$gate_json" || exit 2
610
+ done < <(build_gate_list)
611
+
612
+ print_results_summary
613
+ log_info "All $PHASE-deploy gates completed successfully for target '$TARGET_NAME'."
614
+ }
615
+
616
+ main "$@"