@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,208 @@
1
+ #!/usr/bin/env bash
2
+ # droplet_pipeline.sh — Generate (or update) .github/workflows/publish-droplet.yml
3
+ # and commit it. With --dry-run, print the YAML to stdout only.
4
+ #
5
+ # Usage:
6
+ # droplet_pipeline.sh --target <name> [--config <path>] [--dry-run] [--non-interactive]
7
+ #
8
+ # Exit codes:
9
+ # 0 success
10
+ # 2 pre-flight failure (bad args, missing tools)
11
+ # 4 validation failure (missing required config fields)
12
+
13
+ set -euo pipefail
14
+
15
+ EXIT_PRE_FLIGHT=2
16
+ EXIT_ENV_INVALID=4
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Resolve repo root
22
+ # ---------------------------------------------------------------------------
23
+ if REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel 2>/dev/null)"; then
24
+ :
25
+ else
26
+ REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
27
+ fi
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Source shared helpers
31
+ # ---------------------------------------------------------------------------
32
+ # shellcheck source=publish_common.sh
33
+ source "${SCRIPT_DIR}/publish_common.sh"
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Arg parsing
37
+ # ---------------------------------------------------------------------------
38
+ TARGET_NAME=""
39
+ CONFIG_PATH=""
40
+ DRY_RUN=0
41
+ NON_INTERACTIVE=0
42
+
43
+ usage() {
44
+ cat <<'USAGE'
45
+ Usage: droplet_pipeline.sh --target <name> [--config <path>] [--dry-run] [--non-interactive]
46
+ USAGE
47
+ }
48
+
49
+ fail_pre() {
50
+ printf 'droplet pipeline pre-flight failure: %s\n' "$1" >&2
51
+ exit "${EXIT_PRE_FLIGHT}"
52
+ }
53
+
54
+ while [[ $# -gt 0 ]]; do
55
+ case "$1" in
56
+ --target)
57
+ [[ $# -ge 2 ]] || fail_pre "--target requires a value"
58
+ TARGET_NAME="$2"
59
+ shift 2
60
+ ;;
61
+ --target=*)
62
+ TARGET_NAME="${1#*=}"
63
+ shift
64
+ ;;
65
+ --config)
66
+ [[ $# -ge 2 ]] || fail_pre "--config requires a value"
67
+ CONFIG_PATH="$2"
68
+ shift 2
69
+ ;;
70
+ --config=*)
71
+ CONFIG_PATH="${1#*=}"
72
+ shift
73
+ ;;
74
+ --dry-run)
75
+ DRY_RUN=1
76
+ shift
77
+ ;;
78
+ --non-interactive)
79
+ NON_INTERACTIVE=1
80
+ shift
81
+ ;;
82
+ -h|--help)
83
+ usage
84
+ exit 0
85
+ ;;
86
+ *)
87
+ usage >&2
88
+ fail_pre "unknown option: $1"
89
+ ;;
90
+ esac
91
+ done
92
+
93
+ # Silence unused-variable warning; flag is reserved for CI callers.
94
+ : "${NON_INTERACTIVE:=0}"
95
+
96
+ [[ -n "${TARGET_NAME}" ]] || fail_pre "--target <name> is required"
97
+
98
+ # ---------------------------------------------------------------------------
99
+ # Resolve config path
100
+ # ---------------------------------------------------------------------------
101
+ if [[ -z "${CONFIG_PATH}" ]]; then
102
+ if [[ -f "${REPO_ROOT}/publish.json" ]]; then
103
+ CONFIG_PATH="${REPO_ROOT}/publish.json"
104
+ elif [[ -f "${REPO_ROOT}/project/configs/publish.json" ]]; then
105
+ CONFIG_PATH="${REPO_ROOT}/project/configs/publish.json"
106
+ else
107
+ fail_pre "publish.json not found at '${REPO_ROOT}/publish.json' or '${REPO_ROOT}/project/configs/publish.json'"
108
+ fi
109
+ fi
110
+
111
+ [[ -f "${CONFIG_PATH}" ]] || fail_pre "publish config not found at '${CONFIG_PATH}'"
112
+ command -v jq >/dev/null 2>&1 || fail_pre "jq is required but not found on PATH"
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Validate
116
+ # ---------------------------------------------------------------------------
117
+ if ! bash "${SCRIPT_DIR}/validate_droplet_env.sh" "${CONFIG_PATH}" "${TARGET_NAME}"; then
118
+ exit "${EXIT_ENV_INVALID}"
119
+ fi
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Extract config values
123
+ # ---------------------------------------------------------------------------
124
+ TARGET_JSON="$(jq -c --arg t "${TARGET_NAME}" \
125
+ '.targets[]? | select(.name == $t and .type == "droplet")' \
126
+ "${CONFIG_PATH}")"
127
+
128
+ DEPLOY_PATH="$(printf '%s' "${TARGET_JSON}" | jq -r '.droplet.deploy_path')"
129
+ DEPLOY_BRANCH="$(printf '%s' "${TARGET_JSON}" | jq -r '.droplet.deploy_branch')"
130
+ BUILD_CMD="$(printf '%s' "${TARGET_JSON}" | jq -r '.droplet.build_cmd // empty')"
131
+ START_CMD="$(printf '%s' "${TARGET_JSON}" | jq -r '.droplet.start_cmd')"
132
+ GITHUB_REPO="$(printf '%s' "${TARGET_JSON}" | jq -r '.droplet.github_repo')"
133
+ SSH_KEY_SECRET="$(printf '%s' "${TARGET_JSON}" | jq -r '.secrets.ssh_key_secret')"
134
+ KNOWN_HOSTS_SECRET="$(printf '%s' "${TARGET_JSON}" | jq -r '.secrets.known_hosts_secret')"
135
+
136
+ # Conventional GH Actions secret names for host/user/port
137
+ SSH_HOST_SECRET="DROPLET_SSH_HOST"
138
+ SSH_USER_SECRET="DROPLET_SSH_USER"
139
+ SSH_PORT_SECRET="DROPLET_SSH_PORT"
140
+
141
+ # Keep GITHUB_REPO available for callers / future gh-trigger use.
142
+ : "${GITHUB_REPO}"
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Generate workflow YAML
146
+ # ---------------------------------------------------------------------------
147
+ WORKFLOW_HEADER="$(cat <<EOF
148
+ # Generated by /publish droplet pipeline -- do not edit manually
149
+ name: "Publish -- ${TARGET_NAME} (Droplet)"
150
+ on:
151
+ workflow_dispatch:
152
+
153
+ jobs:
154
+ deploy:
155
+ runs-on: ubuntu-latest
156
+ steps:
157
+ - name: Deploy via SSH
158
+ # appleboy/ssh-action pinned SHA: 4a13f365c4cb2c5a7b0a2e0a54db7a4a7b5f6fd1
159
+ uses: appleboy/ssh-action@4a13f365c4cb2c5a7b0a2e0a54db7a4a7b5f6fd1
160
+ with:
161
+ host: \${{ secrets.${SSH_HOST_SECRET} }}
162
+ username: \${{ secrets.${SSH_USER_SECRET} }}
163
+ port: \${{ secrets.${SSH_PORT_SECRET} }}
164
+ key: \${{ secrets.${SSH_KEY_SECRET} }}
165
+ known_hosts: \${{ secrets.${KNOWN_HOSTS_SECRET} }}
166
+ script: |
167
+ cd ${DEPLOY_PATH}
168
+ git pull origin ${DEPLOY_BRANCH}
169
+ EOF
170
+ )"
171
+
172
+ # Build the script block lines separately so newlines are always present
173
+ SCRIPT_LINES=""
174
+ if [[ -n "${BUILD_CMD}" ]]; then
175
+ SCRIPT_LINES+=" ${BUILD_CMD}"$'\n'
176
+ fi
177
+ SCRIPT_LINES+=" ${START_CMD}"$'\n'
178
+
179
+ WORKFLOW_YAML="${WORKFLOW_HEADER}"$'\n'"${SCRIPT_LINES}"
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Dry-run: print and exit
183
+ # ---------------------------------------------------------------------------
184
+ if (( DRY_RUN )); then
185
+ printf '%s' "${WORKFLOW_YAML}"
186
+ exit 0
187
+ fi
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Write workflow file
191
+ # ---------------------------------------------------------------------------
192
+ WORKFLOW_DIR="${REPO_ROOT}/.github/workflows"
193
+ WORKFLOW_FILE="${WORKFLOW_DIR}/publish-droplet.yml"
194
+
195
+ mkdir -p "${WORKFLOW_DIR}"
196
+ printf '%s' "${WORKFLOW_YAML}" > "${WORKFLOW_FILE}"
197
+
198
+ # Commit only when the file actually changed
199
+ if git -C "${REPO_ROOT}" diff --quiet -- ".github/workflows/publish-droplet.yml" 2>/dev/null && \
200
+ git -C "${REPO_ROOT}" diff --cached --quiet -- ".github/workflows/publish-droplet.yml" 2>/dev/null && \
201
+ git -C "${REPO_ROOT}" ls-files --error-unmatch ".github/workflows/publish-droplet.yml" >/dev/null 2>&1; then
202
+ printf 'Workflow unchanged, skipping commit.\n'
203
+ exit 0
204
+ fi
205
+
206
+ git -C "${REPO_ROOT}" add ".github/workflows/publish-droplet.yml"
207
+ git -C "${REPO_ROOT}" commit -m "chore(publish): update droplet workflow for target ${TARGET_NAME}"
208
+ printf 'Workflow committed: %s\n' "${WORKFLOW_FILE}"
@@ -0,0 +1,200 @@
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: generate_release_notes.sh [--target <name>] [--from-tag <tag>] [--to-ref <git-ref>] [--output <path>] [<publish_json_path>]
10
+ USAGE
11
+ }
12
+
13
+ TARGET_NAME=""
14
+ FROM_TAG=""
15
+ TO_REF="HEAD"
16
+ OUTPUT_PATH=""
17
+ CONFIG_PATH=""
18
+ POSITIONAL_COUNT=0
19
+
20
+ while [[ $# -gt 0 ]]; do
21
+ case "$1" in
22
+ --target)
23
+ [[ $# -ge 2 ]] || { usage >&2; exit 1; }
24
+ TARGET_NAME="$2"
25
+ shift 2
26
+ ;;
27
+ --from-tag)
28
+ [[ $# -ge 2 ]] || { usage >&2; exit 1; }
29
+ FROM_TAG="$2"
30
+ shift 2
31
+ ;;
32
+ --to-ref)
33
+ [[ $# -ge 2 ]] || { usage >&2; exit 1; }
34
+ TO_REF="$2"
35
+ shift 2
36
+ ;;
37
+ --output)
38
+ [[ $# -ge 2 ]] || { usage >&2; exit 1; }
39
+ OUTPUT_PATH="$2"
40
+ shift 2
41
+ ;;
42
+ --help|-h)
43
+ usage
44
+ exit 0
45
+ ;;
46
+ --*)
47
+ usage >&2
48
+ exit 1
49
+ ;;
50
+ *)
51
+ POSITIONAL_COUNT=$((POSITIONAL_COUNT + 1))
52
+ if (( POSITIONAL_COUNT > 1 )); then
53
+ usage >&2
54
+ exit 1
55
+ fi
56
+ CONFIG_PATH="$1"
57
+ shift
58
+ ;;
59
+ esac
60
+ done
61
+
62
+ command -v jq >/dev/null 2>&1 || { echo 'jq is required.' >&2; exit 1; }
63
+
64
+ git rev-parse --verify "$TO_REF" >/dev/null 2>&1 || {
65
+ echo "Unknown git ref: $TO_REF" >&2
66
+ exit 1
67
+ }
68
+
69
+ HISTORY_FILE="$(publish_resolve_history_file "$CONFIG_PATH")"
70
+ LAST_TAG="$FROM_TAG"
71
+ FIRST_RELEASE=0
72
+ CUTOFF_DATE=""
73
+
74
+ if [[ -z "$LAST_TAG" ]]; then
75
+ if LAST_TAG="$(publish_resolve_last_publish_tag "$HISTORY_FILE" "$TO_REF" 2>/dev/null || true)" && [[ -n "$LAST_TAG" ]]; then
76
+ :
77
+ else
78
+ LAST_TAG=""
79
+ FIRST_RELEASE=1
80
+ fi
81
+ fi
82
+
83
+ if [[ -n "$LAST_TAG" ]]; then
84
+ git rev-parse --verify "$LAST_TAG" >/dev/null 2>&1 || {
85
+ echo "Unknown git tag: $LAST_TAG" >&2
86
+ exit 1
87
+ }
88
+ CUTOFF_DATE="$(git log -1 --format=%cI "$LAST_TAG" 2>/dev/null | cut -dT -f1)"
89
+ fi
90
+
91
+ FEATURES=()
92
+ BUG_FIXES=()
93
+ OTHER=()
94
+
95
+ if [[ -n "$LAST_TAG" ]]; then
96
+ LOG_RANGE=("$LAST_TAG..$TO_REF")
97
+ else
98
+ LOG_RANGE=("$TO_REF")
99
+ fi
100
+
101
+ while IFS=$'\t' read -r subject short_sha || [[ -n "${subject:-}${short_sha:-}" ]]; do
102
+ [[ -z "${subject:-}" ]] && continue
103
+ line="- $subject ($short_sha)"
104
+ if printf '%s\n' "$subject" | grep -Eq '^feat(\([^)]+\))?!?: '; then
105
+ FEATURES+=("$line")
106
+ elif printf '%s\n' "$subject" | grep -Eq '^fix(\([^)]+\))?!?: '; then
107
+ BUG_FIXES+=("$line")
108
+ else
109
+ OTHER+=("$line")
110
+ fi
111
+ done < <(git log "${LOG_RANGE[@]}" --pretty=format:'%s%x09%h')
112
+
113
+ collect_completed_tasks() {
114
+ local cutoff_date="$1"
115
+ local tasks_dir="$PUBLISH_REPO_ROOT/project/board/tasks"
116
+ local collected=()
117
+ local task_file status completed_date task_id task_title
118
+
119
+ [[ -d "$tasks_dir" ]] || return 0
120
+
121
+ while IFS= read -r -d '' task_file; do
122
+ case "$task_file" in
123
+ *_INSTRUCTIONS.md) continue ;;
124
+ esac
125
+
126
+ status="$(awk '/^status: /{sub(/^status: /, ""); print; exit}' "$task_file" 2>/dev/null || true)"
127
+ [[ "$status" == "Passed" ]] || continue
128
+
129
+ completed_date="$(awk '/^date_completed: /{sub(/^date_completed: /, ""); print; exit}' "$task_file" 2>/dev/null || true)"
130
+ [[ -n "$completed_date" ]] || continue
131
+
132
+ if [[ -n "$cutoff_date" && "$completed_date" < "$cutoff_date" ]]; then
133
+ continue
134
+ fi
135
+
136
+ task_id="$(awk '/^id: /{sub(/^id: /, ""); print; exit}' "$task_file" 2>/dev/null || true)"
137
+ task_title="$(awk '/^title: /{sub(/^title: /, ""); print; exit}' "$task_file" 2>/dev/null || true)"
138
+ [[ -n "$task_id" ]] || continue
139
+ [[ -n "$task_title" ]] || task_title="$task_id"
140
+ collected+=("- $task_id — $task_title")
141
+ done < <(find "$tasks_dir" -maxdepth 1 -type f -name '*.md' -print0 2>/dev/null)
142
+
143
+ if (( ${#collected[@]} > 0 )); then
144
+ printf '%s\n' "${collected[@]}" | LC_ALL=C sort -u
145
+ fi
146
+ }
147
+
148
+ if [[ -z "$OUTPUT_PATH" ]]; then
149
+ OUTPUT_PATH="$(publish_temp_release_notes_path)"
150
+ fi
151
+ mkdir -p "$(dirname "$OUTPUT_PATH")"
152
+
153
+ write_section() {
154
+ local heading="$1"
155
+ local array_name="$2"
156
+ local item_count
157
+
158
+ eval "item_count=\${#${array_name}[@]}"
159
+ {
160
+ printf '### %s\n' "$heading"
161
+ if (( item_count > 0 )); then
162
+ eval "printf '%s\\n' \"\${${array_name}[@]}\""
163
+ else
164
+ printf -- '- None\n'
165
+ fi
166
+ printf '\n'
167
+ } >> "$OUTPUT_PATH"
168
+ }
169
+
170
+ GENERATED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
171
+ {
172
+ printf '# Release notes\n\n'
173
+ printf -- '- Generated: %s\n' "$GENERATED_AT"
174
+ if [[ -n "$TARGET_NAME" ]]; then
175
+ printf -- '- Target: %s\n' "$TARGET_NAME"
176
+ fi
177
+ if [[ -n "$LAST_TAG" ]]; then
178
+ printf -- '- Changes: %s..%s\n\n' "$LAST_TAG" "$TO_REF"
179
+ else
180
+ printf -- '- Changes: full history through %s\n\n' "$TO_REF"
181
+ if (( FIRST_RELEASE )); then
182
+ printf '> First release — full history included\n\n'
183
+ fi
184
+ fi
185
+ } > "$OUTPUT_PATH"
186
+
187
+ write_section 'Features' FEATURES
188
+ write_section 'Bug Fixes' BUG_FIXES
189
+ write_section 'Other' OTHER
190
+
191
+ COMPLETED_TASKS="$(collect_completed_tasks "$CUTOFF_DATE" || true)"
192
+ if [[ -n "$COMPLETED_TASKS" ]]; then
193
+ {
194
+ printf '### Completed tasks\n'
195
+ printf '%s\n' "$COMPLETED_TASKS"
196
+ printf '\n'
197
+ } >> "$OUTPUT_PATH"
198
+ fi
199
+
200
+ printf '📝 Release notes draft written to %s\n' "$OUTPUT_PATH"