@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,538 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/publish_common.sh"
6
+
7
+ VALIDATE_CONFIG_SCRIPT="$SCRIPT_DIR/validate_config.sh"
8
+ CHECK_TARGET_CONFIG_SCRIPT="$SCRIPT_DIR/check_target_config.sh"
9
+ SETUP_WIZARD_SCRIPT="$SCRIPT_DIR/setup_wizard.sh"
10
+ VALIDATE_IOS_ENV_SCRIPT="$SCRIPT_DIR/validate_ios_env.sh"
11
+ VALIDATE_NPM_ENV_SCRIPT="$SCRIPT_DIR/validate_npm_env.sh"
12
+ VALIDATE_DROPLET_ENV_SCRIPT="$SCRIPT_DIR/validate_droplet_env.sh"
13
+ VALIDATE_NPM_CI_ENV_SCRIPT="$SCRIPT_DIR/validate_npm_ci_env.sh"
14
+ DROPLET_PIPELINE_SCRIPT="$SCRIPT_DIR/droplet_pipeline.sh"
15
+ NPM_CI_PIPELINE_SCRIPT="$SCRIPT_DIR/npm_ci_pipeline.sh"
16
+ RUN_GATES_SCRIPT="$SCRIPT_DIR/run_gates.sh"
17
+ GENERATE_RELEASE_NOTES_SCRIPT="$SCRIPT_DIR/generate_release_notes.sh"
18
+ SUGGEST_SEMVER_SCRIPT="$SCRIPT_DIR/suggest_semver_bump.sh"
19
+ WRITE_LEDGER_ENTRY_SCRIPT="$SCRIPT_DIR/write_ledger_entry.sh"
20
+ IOS_PIPELINE_SCRIPT="$SCRIPT_DIR/ios_pipeline.sh"
21
+ NPM_PIPELINE_SCRIPT="$SCRIPT_DIR/npm_pipeline.sh"
22
+
23
+ EXIT_ABORTED=1
24
+ EXIT_GATE_FAILURE=2
25
+ EXIT_ADAPTER_FAILURE=3
26
+ EXIT_CONFIG_INVALID=4
27
+
28
+ TARGET_NAME=""
29
+ CONFIG_PATH=""
30
+ AUTO_YES=0
31
+ DRY_RUN=0
32
+ BUMP_OVERRIDE=""
33
+ RELEASE_NOTES_PATH=""
34
+ PROMPT_INPUT_FD=0
35
+ NON_INTERACTIVE=0
36
+ TARGET_TYPE=""
37
+ HISTORY_FILE=""
38
+ LAST_TAG=""
39
+ SELECTED_VERSION=""
40
+
41
+ usage() {
42
+ cat <<'USAGE'
43
+ Usage: publish_deploy.sh [--target <name>] [--config <path>] [--yes] [--dry-run]
44
+ [--minor | --major] [--release-notes <path>]
45
+ USAGE
46
+ }
47
+
48
+ log_info() {
49
+ printf '→ %s\n' "$1"
50
+ }
51
+
52
+ log_warn() {
53
+ printf '⚠ %s\n' "$1" >&2
54
+ }
55
+
56
+ fail_with() {
57
+ local exit_code="$1"
58
+ shift
59
+ printf '%s\n' "$*" >&2
60
+ exit "$exit_code"
61
+ }
62
+
63
+ open_prompt_fd() {
64
+ if [[ -t 0 ]]; then
65
+ if exec 3</dev/tty 2>/dev/null; then
66
+ PROMPT_INPUT_FD=3
67
+ NON_INTERACTIVE=0
68
+ return 0
69
+ fi
70
+ fi
71
+ NON_INTERACTIVE=1
72
+ }
73
+
74
+ prompt_line() {
75
+ local prompt="$1"
76
+ local response=""
77
+ printf '%s' "$prompt" >&2
78
+ if ! IFS= read -r -u "$PROMPT_INPUT_FD" response; then
79
+ printf '\n' >&2
80
+ exit "$EXIT_ABORTED"
81
+ fi
82
+ printf '%s\n' "$response"
83
+ }
84
+
85
+ confirm_or_abort() {
86
+ local prompt="$1"
87
+ local response
88
+ response="$(prompt_line "$prompt")"
89
+ case "${response:-Y}" in
90
+ y|Y|yes|YES)
91
+ return 0
92
+ ;;
93
+ *)
94
+ exit "$EXIT_ABORTED"
95
+ ;;
96
+ esac
97
+ }
98
+
99
+ resolve_default_config_path() {
100
+ if [[ -n "$CONFIG_PATH" ]]; then
101
+ publish_normalize_path "$CONFIG_PATH"
102
+ return 0
103
+ fi
104
+
105
+ if [[ -f "$PUBLISH_REPO_ROOT/publish.json" ]]; then
106
+ printf '%s\n' "$PUBLISH_REPO_ROOT/publish.json"
107
+ return 0
108
+ fi
109
+
110
+ if [[ -f "$PUBLISH_REPO_ROOT/project/configs/publish.json" ]]; then
111
+ printf '%s\n' "$PUBLISH_REPO_ROOT/project/configs/publish.json"
112
+ return 0
113
+ fi
114
+
115
+ printf '%s\n' "$PUBLISH_REPO_ROOT/publish.json"
116
+ }
117
+
118
+ validate_config_file() {
119
+ [[ -f "$CONFIG_PATH" ]] || fail_with "$EXIT_CONFIG_INVALID" "publish.json not found at '$CONFIG_PATH'."
120
+ command -v jq >/dev/null 2>&1 || fail_with "$EXIT_CONFIG_INVALID" "jq is required."
121
+ jq -e . "$CONFIG_PATH" >/dev/null 2>&1 || fail_with "$EXIT_CONFIG_INVALID" "publish.json is not valid JSON."
122
+ }
123
+
124
+ select_target_from_config() {
125
+ local targets=()
126
+ while IFS= read -r target; do
127
+ [[ -n "$target" ]] && targets+=("$target")
128
+ done < <(jq -r '.targets[]?.name // empty' "$CONFIG_PATH")
129
+ [[ ${#targets[@]} -gt 0 ]] || fail_with "$EXIT_CONFIG_INVALID" "No targets were found in '$CONFIG_PATH'."
130
+
131
+ echo "Available publish targets:" >&2
132
+ local index=1
133
+ local target
134
+ for target in "${targets[@]}"; do
135
+ printf ' %s. %s\n' "$index" "$target" >&2
136
+ index=$((index + 1))
137
+ done
138
+
139
+ while true; do
140
+ local selection
141
+ selection="$(prompt_line "Select target [1-${#targets[@]}]: ")"
142
+ [[ "$selection" =~ ^[0-9]+$ ]] || {
143
+ echo "Enter a target number." >&2
144
+ continue
145
+ }
146
+ if (( selection < 1 || selection > ${#targets[@]} )); then
147
+ echo "Selection out of range." >&2
148
+ continue
149
+ fi
150
+ TARGET_NAME="${targets[$((selection - 1))]}"
151
+ return 0
152
+ done
153
+ }
154
+
155
+ resolve_target_type() {
156
+ TARGET_TYPE="$(jq -r --arg target "$TARGET_NAME" '.targets[]? | select(.name == $target) | .type // empty' "$CONFIG_PATH" | head -n 1)"
157
+ [[ -n "$TARGET_TYPE" ]] || TARGET_TYPE="mobile-ios"
158
+ }
159
+
160
+ ensure_target_selected() {
161
+ if [[ -n "$TARGET_NAME" ]]; then
162
+ return 0
163
+ fi
164
+
165
+ if (( AUTO_YES || NON_INTERACTIVE )); then
166
+ fail_with "$EXIT_CONFIG_INVALID" "Non-interactive deploy requires --target <name>."
167
+ fi
168
+
169
+ select_target_from_config
170
+ }
171
+
172
+ run_target_completeness_check() {
173
+ local wizard_ran=0
174
+ while true; do
175
+ if bash "$CHECK_TARGET_CONFIG_SCRIPT" "$TARGET_NAME" "$CONFIG_PATH"; then
176
+ return 0
177
+ fi
178
+
179
+ if (( AUTO_YES || NON_INTERACTIVE )); then
180
+ exit "$EXIT_CONFIG_INVALID"
181
+ fi
182
+
183
+ if (( wizard_ran )); then
184
+ exit "$EXIT_CONFIG_INVALID"
185
+ fi
186
+
187
+ resolve_target_type
188
+ printf "⚠️ Target '%s' has missing or incomplete config. Running setup wizard...\n" "$TARGET_NAME" >&2
189
+ local status=0
190
+ if bash "$SETUP_WIZARD_SCRIPT" "$TARGET_NAME" --type "$TARGET_TYPE" --config "$CONFIG_PATH"; then
191
+ :
192
+ else
193
+ status=$?
194
+ exit "$status"
195
+ fi
196
+ wizard_ran=1
197
+ validate_config_file
198
+ done
199
+ }
200
+
201
+ validate_target_env() {
202
+ case "$TARGET_TYPE" in
203
+ mobile-ios)
204
+ if ! bash "$VALIDATE_IOS_ENV_SCRIPT" "$CONFIG_PATH" "$TARGET_NAME"; then
205
+ exit "$EXIT_CONFIG_INVALID"
206
+ fi
207
+ ;;
208
+ npm)
209
+ if ! bash "$VALIDATE_NPM_ENV_SCRIPT" "$CONFIG_PATH" "$TARGET_NAME"; then
210
+ exit "$EXIT_CONFIG_INVALID"
211
+ fi
212
+ ;;
213
+ npm-ci)
214
+ if ! bash "$VALIDATE_NPM_CI_ENV_SCRIPT" "$CONFIG_PATH" "$TARGET_NAME"; then
215
+ exit "$EXIT_CONFIG_INVALID"
216
+ fi
217
+ ;;
218
+ droplet)
219
+ if ! bash "$VALIDATE_DROPLET_ENV_SCRIPT" "$CONFIG_PATH" "$TARGET_NAME"; then
220
+ exit "$EXIT_CONFIG_INVALID"
221
+ fi
222
+ ;;
223
+ *)
224
+ fail_with "$EXIT_CONFIG_INVALID" "Unsupported target type '$TARGET_TYPE'."
225
+ ;;
226
+ esac
227
+ }
228
+
229
+ resolve_last_tag() {
230
+ LAST_TAG="$(publish_resolve_last_publish_tag "$HISTORY_FILE" HEAD 2>/dev/null || true)"
231
+ }
232
+
233
+ print_board_summary() {
234
+ local stories_dir="$PUBLISH_REPO_ROOT/project/board/stories"
235
+ local tasks_dir="$PUBLISH_REPO_ROOT/project/board/tasks"
236
+ [[ -d "$stories_dir" || -d "$tasks_dir" ]] || return 0
237
+
238
+ local cutoff_date=""
239
+ if [[ -n "$LAST_TAG" ]]; then
240
+ cutoff_date="$(git log -1 --format=%cI "$LAST_TAG" 2>/dev/null | cut -dT -f1)"
241
+ fi
242
+
243
+ local collected=""
244
+ local file status completed_date item_id item_title label
245
+
246
+ for label in story task; do
247
+ local dir
248
+ if [[ "$label" == "story" ]]; then
249
+ dir="$stories_dir"
250
+ else
251
+ dir="$tasks_dir"
252
+ fi
253
+
254
+ [[ -d "$dir" ]] || continue
255
+ while IFS= read -r -d '' file; do
256
+ case "$file" in
257
+ *_INSTRUCTIONS.md) continue ;;
258
+ esac
259
+ status="$(awk '/^status: /{sub(/^status: /, ""); print; exit}' "$file" 2>/dev/null || true)"
260
+ case "$status" in
261
+ "Passed"|"Passed with remarks") ;;
262
+ *) continue ;;
263
+ esac
264
+ completed_date="$(awk '/^date_completed: /{sub(/^date_completed: /, ""); print; exit}' "$file" 2>/dev/null || true)"
265
+ [[ -n "$completed_date" ]] || continue
266
+ if [[ -n "$cutoff_date" && "$completed_date" < "$cutoff_date" ]]; then
267
+ continue
268
+ fi
269
+ item_id="$(awk '/^id: /{sub(/^id: /, ""); print; exit}' "$file" 2>/dev/null || true)"
270
+ item_title="$(awk '/^title: /{sub(/^title: /, ""); print; exit}' "$file" 2>/dev/null || true)"
271
+ [[ -n "$item_id" ]] || continue
272
+ if [[ "$label" == "story" ]]; then
273
+ collected+="- [Story] $item_id — ${item_title:-$item_id} (${completed_date})"$'\n'
274
+ else
275
+ collected+="- [Task] $item_id — ${item_title:-$item_id} (${completed_date})"$'\n'
276
+ fi
277
+ done < <(find "$dir" -maxdepth 1 -type f -name '*.md' -print0 2>/dev/null)
278
+ done
279
+
280
+ [[ -n "$collected" ]] || return 0
281
+ printf '\nBoard summary since last publish:\n%s\n' "$collected"
282
+ }
283
+
284
+ generate_release_notes() {
285
+ if [[ -n "$RELEASE_NOTES_PATH" ]]; then
286
+ if [[ "$RELEASE_NOTES_PATH" != /* ]]; then
287
+ RELEASE_NOTES_PATH="$PUBLISH_REPO_ROOT/${RELEASE_NOTES_PATH#./}"
288
+ fi
289
+ [[ -f "$RELEASE_NOTES_PATH" ]] || fail_with "$EXIT_CONFIG_INVALID" "Release-notes file not found at '$RELEASE_NOTES_PATH'."
290
+ return 0
291
+ fi
292
+
293
+ RELEASE_NOTES_PATH="$(publish_temp_release_notes_path)"
294
+ local cmd=(bash "$GENERATE_RELEASE_NOTES_SCRIPT" --target "$TARGET_NAME" --output "$RELEASE_NOTES_PATH")
295
+ if [[ -n "$LAST_TAG" ]]; then
296
+ cmd+=(--from-tag "$LAST_TAG")
297
+ fi
298
+ cmd+=("$CONFIG_PATH")
299
+ if ! "${cmd[@]}"; then
300
+ fail_with "$EXIT_ADAPTER_FAILURE" "Failed to generate release notes."
301
+ fi
302
+ }
303
+
304
+ review_release_notes() {
305
+ (( AUTO_YES )) && return 0
306
+
307
+ if [[ -n "${EDITOR:-}" ]]; then
308
+ "${EDITOR}" "$RELEASE_NOTES_PATH"
309
+ elif command -v less >/dev/null 2>&1; then
310
+ less "$RELEASE_NOTES_PATH"
311
+ else
312
+ cat "$RELEASE_NOTES_PATH"
313
+ fi
314
+
315
+ confirm_or_abort "Use these release notes? [Y/n]: "
316
+ }
317
+
318
+ resolve_selected_version() {
319
+ local current_version suggested_version
320
+ current_version="$(publish_latest_ledger_version "$HISTORY_FILE" 2>/dev/null || true)"
321
+ [[ -n "$current_version" ]] || current_version='v0.0.0'
322
+ current_version="$(publish_normalize_version "$current_version")" || current_version='v0.0.0'
323
+
324
+ suggested_version="$(bash "$SUGGEST_SEMVER_SCRIPT" --yes "$CONFIG_PATH" | tail -n 1 | tr -d '\r')"
325
+ suggested_version="$(publish_normalize_version "$suggested_version")" || fail_with "$EXIT_ADAPTER_FAILURE" "Unable to determine a suggested version."
326
+
327
+ case "$BUMP_OVERRIDE" in
328
+ major|minor)
329
+ SELECTED_VERSION="$(publish_bump_version "$current_version" "$BUMP_OVERRIDE")"
330
+ ;;
331
+ *)
332
+ if (( AUTO_YES )); then
333
+ SELECTED_VERSION="$(publish_bump_version "$current_version" patch)"
334
+ else
335
+ SELECTED_VERSION="$suggested_version"
336
+ fi
337
+ ;;
338
+ esac
339
+
340
+ if (( AUTO_YES )); then
341
+ return 0
342
+ fi
343
+
344
+ printf 'Suggested version: %s\n' "$suggested_version"
345
+ if [[ -n "$BUMP_OVERRIDE" ]]; then
346
+ printf 'Using %s override: %s\n' "$BUMP_OVERRIDE" "$SELECTED_VERSION"
347
+ fi
348
+ confirm_or_abort "Use version $SELECTED_VERSION? [Y/n]: "
349
+ }
350
+
351
+ confirm_final_deploy() {
352
+ (( AUTO_YES )) && return 0
353
+ local response
354
+ response="$(prompt_line "Deploy ${SELECTED_VERSION} to ${TARGET_NAME}? [y/N] ")"
355
+ case "${response:-N}" in
356
+ y|Y|yes|YES)
357
+ return 0
358
+ ;;
359
+ *)
360
+ exit "$EXIT_ABORTED"
361
+ ;;
362
+ esac
363
+ }
364
+
365
+ run_pre_gates() {
366
+ local cmd=(bash "$RUN_GATES_SCRIPT" pre "$TARGET_NAME" "$CONFIG_PATH")
367
+ (( AUTO_YES )) && cmd+=(--non-interactive)
368
+ if ! "${cmd[@]}"; then
369
+ exit "$EXIT_GATE_FAILURE"
370
+ fi
371
+ }
372
+
373
+ run_adapter_pipeline() {
374
+ local cmd
375
+ case "$TARGET_TYPE" in
376
+ mobile-ios)
377
+ cmd=(bash "$IOS_PIPELINE_SCRIPT" "$TARGET_NAME" "$CONFIG_PATH")
378
+ ;;
379
+ npm)
380
+ cmd=(bash "$NPM_PIPELINE_SCRIPT" "$TARGET_NAME" "$CONFIG_PATH")
381
+ ;;
382
+ npm-ci)
383
+ cmd=(bash "$NPM_CI_PIPELINE_SCRIPT" --target "$TARGET_NAME" --config "$CONFIG_PATH")
384
+ ;;
385
+ droplet)
386
+ cmd=(bash "$DROPLET_PIPELINE_SCRIPT" --target "$TARGET_NAME" --config "$CONFIG_PATH")
387
+ ;;
388
+ *)
389
+ fail_with "$EXIT_CONFIG_INVALID" "Unsupported target type '$TARGET_TYPE'."
390
+ ;;
391
+ esac
392
+
393
+ (( DRY_RUN )) && cmd+=(--dry-run)
394
+ (( AUTO_YES )) && cmd+=(--non-interactive)
395
+
396
+ local status=0
397
+ if "${cmd[@]}"; then
398
+ return 0
399
+ else
400
+ status=$?
401
+ fi
402
+ if [[ $status -eq $EXIT_CONFIG_INVALID ]]; then
403
+ exit "$EXIT_CONFIG_INVALID"
404
+ fi
405
+ exit "$EXIT_ADAPTER_FAILURE"
406
+ }
407
+
408
+ run_post_gates() {
409
+ local cmd=(bash "$RUN_GATES_SCRIPT" post "$TARGET_NAME" "$CONFIG_PATH")
410
+ (( AUTO_YES )) && cmd+=(--non-interactive)
411
+
412
+ local status=0
413
+ if "${cmd[@]}"; then
414
+ return 0
415
+ else
416
+ status=$?
417
+ fi
418
+ if [[ $status -eq $EXIT_GATE_FAILURE ]]; then
419
+ log_warn "Post-deploy gates failed. The deploy will be recorded as partial."
420
+ else
421
+ log_warn "Post-deploy gate runner exited with status $status. The deploy will be recorded as partial."
422
+ fi
423
+ return 1
424
+ }
425
+
426
+ write_ledger_entry() {
427
+ local platform_state="$1"
428
+ local cmd=(bash "$WRITE_LEDGER_ENTRY_SCRIPT" "$TARGET_NAME" "$TARGET_TYPE" "$platform_state" "$RELEASE_NOTES_PATH" --version "$SELECTED_VERSION" --config "$CONFIG_PATH")
429
+ (( AUTO_YES )) && cmd+=(--yes)
430
+ (( DRY_RUN )) && cmd+=(--dry-run)
431
+ "${cmd[@]}"
432
+ }
433
+
434
+ print_manual_steps() {
435
+ local adapter_doc
436
+ case "$TARGET_TYPE" in
437
+ droplet)
438
+ adapter_doc="$PUBLISH_REPO_ROOT/skills/publish/adapters/droplet.md"
439
+ ;;
440
+ *)
441
+ adapter_doc="$PUBLISH_REPO_ROOT/skills/publish/adapters/mobile-ios.md"
442
+ ;;
443
+ esac
444
+ [[ -f "$adapter_doc" ]] || return 0
445
+ local manual_steps
446
+ manual_steps="$(awk '
447
+ /^## Post-deploy manual steps$/ {capture=1; next}
448
+ capture && /^## / {exit}
449
+ capture {print}
450
+ ' "$adapter_doc")"
451
+ [[ -n "$manual_steps" ]] || return 0
452
+ printf '\n%s\n' "$manual_steps"
453
+ }
454
+
455
+ parse_args() {
456
+ while [[ $# -gt 0 ]]; do
457
+ case "$1" in
458
+ --target)
459
+ [[ $# -ge 2 ]] || { usage >&2; exit 1; }
460
+ TARGET_NAME="$2"
461
+ shift 2
462
+ ;;
463
+ --config)
464
+ [[ $# -ge 2 ]] || { usage >&2; exit 1; }
465
+ CONFIG_PATH="$2"
466
+ shift 2
467
+ ;;
468
+ --yes)
469
+ AUTO_YES=1
470
+ shift
471
+ ;;
472
+ --dry-run)
473
+ DRY_RUN=1
474
+ shift
475
+ ;;
476
+ --minor)
477
+ [[ -z "$BUMP_OVERRIDE" ]] || { usage >&2; exit 1; }
478
+ BUMP_OVERRIDE="minor"
479
+ shift
480
+ ;;
481
+ --major)
482
+ [[ -z "$BUMP_OVERRIDE" ]] || { usage >&2; exit 1; }
483
+ BUMP_OVERRIDE="major"
484
+ shift
485
+ ;;
486
+ --release-notes)
487
+ [[ $# -ge 2 ]] || { usage >&2; exit 1; }
488
+ RELEASE_NOTES_PATH="$2"
489
+ shift 2
490
+ ;;
491
+ --help|-h)
492
+ usage
493
+ exit 0
494
+ ;;
495
+ *)
496
+ usage >&2
497
+ exit 1
498
+ ;;
499
+ esac
500
+ done
501
+ }
502
+
503
+ main() {
504
+ parse_args "$@"
505
+ open_prompt_fd
506
+
507
+ CONFIG_PATH="$(resolve_default_config_path)"
508
+ HISTORY_FILE="$(publish_resolve_history_file "$CONFIG_PATH")"
509
+
510
+ (( DRY_RUN )) && printf '🔍 DRY RUN — no real deployment will occur\n'
511
+
512
+ validate_config_file
513
+ ensure_target_selected
514
+ run_target_completeness_check
515
+ resolve_target_type
516
+ validate_target_env
517
+ resolve_last_tag
518
+ print_board_summary
519
+ run_pre_gates
520
+ generate_release_notes
521
+ review_release_notes
522
+ resolve_selected_version
523
+ confirm_final_deploy
524
+ run_adapter_pipeline
525
+
526
+ local platform_state="uploaded"
527
+ if ! run_post_gates; then
528
+ platform_state="partial"
529
+ fi
530
+ if (( DRY_RUN )); then
531
+ platform_state="dry-run"
532
+ fi
533
+
534
+ write_ledger_entry "$platform_state"
535
+ print_manual_steps
536
+ }
537
+
538
+ main "$@"
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/publish_common.sh"
6
+
7
+ usage() {
8
+ cat <<'USAGE'
9
+ Usage: reconcile_tags.sh [--dry-run] [--config <path>]
10
+ USAGE
11
+ }
12
+
13
+ DRY_RUN=0
14
+ CONFIG_PATH=''
15
+
16
+ while [[ $# -gt 0 ]]; do
17
+ case "$1" in
18
+ --dry-run)
19
+ DRY_RUN=1
20
+ shift
21
+ ;;
22
+ --config)
23
+ [[ $# -ge 2 ]] || { usage >&2; exit 1; }
24
+ CONFIG_PATH="$2"
25
+ shift 2
26
+ ;;
27
+ --help|-h)
28
+ usage
29
+ exit 0
30
+ ;;
31
+ *)
32
+ usage >&2
33
+ exit 1
34
+ ;;
35
+ esac
36
+ done
37
+
38
+ command -v jq >/dev/null 2>&1 || { echo 'jq is required.' >&2; exit 1; }
39
+ HISTORY_FILE="$(publish_resolve_history_file "$CONFIG_PATH")"
40
+ if (( DRY_RUN == 0 )); then
41
+ publish_ensure_history_file "$HISTORY_FILE"
42
+ fi
43
+
44
+ TAGS=()
45
+ while IFS= read -r tag; do
46
+ [[ -n "$tag" ]] && TAGS+=("$tag")
47
+ done < <(publish_list_semver_tags HEAD)
48
+
49
+ LEDGER_VERSIONS=()
50
+ if [[ -f "$HISTORY_FILE" ]]; then
51
+ while IFS= read -r version; do
52
+ [[ -n "$version" ]] && LEDGER_VERSIONS+=("$version")
53
+ done < <(jq -r '.[]? | .version? | strings | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+$"))' "$HISTORY_FILE")
54
+ fi
55
+
56
+ contains_value() {
57
+ local needle="$1"
58
+ shift
59
+ local value
60
+ for value in "$@"; do
61
+ [[ "$value" == "$needle" ]] && return 0
62
+ done
63
+ return 1
64
+ }
65
+
66
+ NEW_LEDGER_ENTRIES=0
67
+ NEW_GIT_TAGS=0
68
+
69
+ for tag in "${TAGS[@]:-}"; do
70
+ [[ -n "$tag" ]] || continue
71
+ if contains_value "$tag" "${LEDGER_VERSIONS[@]:-}"; then
72
+ continue
73
+ fi
74
+
75
+ NEW_LEDGER_ENTRIES=$((NEW_LEDGER_ENTRIES + 1))
76
+ if (( DRY_RUN )); then
77
+ printf '⚠️ Tag %s found without ledger entry — would create partial entry\n' "$tag"
78
+ continue
79
+ fi
80
+
81
+ TAG_TIMESTAMP="$(git for-each-ref --format='%(creatordate:iso8601-strict)' "refs/tags/$tag" | head -n 1)"
82
+ [[ -n "$TAG_TIMESTAMP" ]] || TAG_TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
83
+ TAG_ACTOR="$(git for-each-ref --format='%(taggername)' "refs/tags/$tag" | head -n 1)"
84
+ [[ -n "$TAG_ACTOR" ]] || TAG_ACTOR='unknown'
85
+ TAG_COMMIT="$(git rev-list -n 1 "$tag")"
86
+ ENTRY_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
87
+ ENTRY_JSON="$(jq -cn \
88
+ --arg id "$ENTRY_ID" \
89
+ --arg version "$tag" \
90
+ --arg target 'unknown' \
91
+ --arg adapter 'manual' \
92
+ --arg timestamp "$TAG_TIMESTAMP" \
93
+ --arg released_by "$TAG_ACTOR" \
94
+ --arg platform_state 'partial' \
95
+ --arg git_tag "$tag" \
96
+ --arg commit_sha "$TAG_COMMIT" \
97
+ --arg note 'Manually tagged without /publish' \
98
+ '{
99
+ id: $id,
100
+ version: $version,
101
+ target: $target,
102
+ adapter: $adapter,
103
+ timestamp: $timestamp,
104
+ released_by: $released_by,
105
+ release_notes_path: null,
106
+ platform_state: $platform_state,
107
+ git_tag: $git_tag,
108
+ commit_sha: $commit_sha,
109
+ note: $note
110
+ }')"
111
+ publish_append_history_entry "$HISTORY_FILE" "$ENTRY_JSON"
112
+ printf '⚠️ Tag %s found without ledger entry — created partial entry\n' "$tag"
113
+ done
114
+
115
+ if [[ -f "$HISTORY_FILE" ]]; then
116
+ while IFS=$'\t' read -r version commit_sha; do
117
+ [[ -n "$version" ]] || continue
118
+ if contains_value "$version" "${TAGS[@]:-}"; then
119
+ continue
120
+ fi
121
+
122
+ NEW_GIT_TAGS=$((NEW_GIT_TAGS + 1))
123
+ TAG_COMMIT="${commit_sha:-$(git rev-parse HEAD)}"
124
+
125
+ if (( DRY_RUN )); then
126
+ printf '⚠️ Ledger entry %s had no git tag — would create retroactively\n' "$version"
127
+ continue
128
+ fi
129
+
130
+ git tag -a "$version" "$TAG_COMMIT" -m 'Retroactive tag from /publish ledger'
131
+ printf '⚠️ Ledger entry %s had no git tag — created retroactively\n' "$version"
132
+ done < <(jq -r '.[]? | select(type == "object") | select((.version? // "") | test("^v[0-9]+\\.[0-9]+\\.[0-9]+$")) | [(.version // ""), (.commit_sha // "")] | @tsv' "$HISTORY_FILE")
133
+ fi
134
+
135
+ printf 'Reconciliation complete. %s new ledger entries, %s new git tags.\n' "$NEW_LEDGER_ENTRIES" "$NEW_GIT_TAGS"