@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,249 @@
1
+ #!/usr/bin/env bash
2
+ # npm_pipeline.sh — Execute the npm publish pipeline for the JengaAgent repo.
3
+ #
4
+ # Runs from the JengaAgent repo root (where package.json lives) and invokes
5
+ # `npm publish --tag <dist_tag>` (or `--dry-run` when requested).
6
+ #
7
+ # Exit codes:
8
+ # 0 success (live publish or dry-run completed)
9
+ # 2 pre-publish gate failure (missing package.json, missing npm/node, bad args)
10
+ # 3 publish error (`npm publish` returned non-zero)
11
+ #
12
+ # Style and conventions mirror `ios_pipeline.sh`.
13
+
14
+ set -euo pipefail
15
+
16
+ EXIT_PRE_PUBLISH_FAILURE=2
17
+ EXIT_PUBLISH_FAILURE=3
18
+
19
+ usage() {
20
+ cat <<'USAGE'
21
+ Usage: npm_pipeline.sh [<target> <publish_json_path>] [--dry-run] [--dist-tag <tag>] [--non-interactive]
22
+
23
+ Options:
24
+ <target> Optional target name from publish.json (npm target).
25
+ <publish_json_path> Optional path to publish.json for reading defaults.
26
+ --dry-run Run `npm publish --dry-run`; do not touch the registry.
27
+ --dist-tag <tag> Override dist-tag. Falls back to $NPM_DIST_TAG, then
28
+ to the target's `npm.dist_tag`, then to "latest".
29
+ --non-interactive Reserved flag for CI; no interactive prompts today.
30
+ -h, --help Show this message.
31
+ USAGE
32
+ }
33
+
34
+ log_info() {
35
+ printf '→ %s\n' "$1"
36
+ }
37
+
38
+ log_warn() {
39
+ printf '⚠ %s\n' "$1" >&2
40
+ }
41
+
42
+ fail_pre_publish() {
43
+ printf 'npm pipeline pre-publish failure: %s\n' "$1" >&2
44
+ exit "$EXIT_PRE_PUBLISH_FAILURE"
45
+ }
46
+
47
+ fail_publish() {
48
+ printf 'npm pipeline publish failure: %s\n' "$1" >&2
49
+ exit "$EXIT_PUBLISH_FAILURE"
50
+ }
51
+
52
+ TARGET_NAME=""
53
+ PUBLISH_JSON=""
54
+ DRY_RUN=0
55
+ NON_INTERACTIVE=0
56
+ DIST_TAG_ARG=""
57
+
58
+ # Peel off optional positional args before flag parsing so callers may pass
59
+ # `<target> <publish_json_path>` in either order relative to flags.
60
+ POSITIONAL=()
61
+ while (($#)); do
62
+ case "$1" in
63
+ -h|--help)
64
+ usage
65
+ exit 0
66
+ ;;
67
+ --dry-run)
68
+ DRY_RUN=1
69
+ shift
70
+ ;;
71
+ --non-interactive)
72
+ NON_INTERACTIVE=1
73
+ shift
74
+ ;;
75
+ --dist-tag)
76
+ [[ $# -ge 2 ]] || fail_pre_publish "--dist-tag requires a value"
77
+ DIST_TAG_ARG="$2"
78
+ shift 2
79
+ ;;
80
+ --dist-tag=*)
81
+ DIST_TAG_ARG="${1#*=}"
82
+ shift
83
+ ;;
84
+ --)
85
+ shift
86
+ while (($#)); do
87
+ POSITIONAL+=("$1")
88
+ shift
89
+ done
90
+ ;;
91
+ -*)
92
+ usage >&2
93
+ fail_pre_publish "unknown option: $1"
94
+ ;;
95
+ *)
96
+ POSITIONAL+=("$1")
97
+ shift
98
+ ;;
99
+ esac
100
+ done
101
+
102
+ if [[ ${#POSITIONAL[@]} -ge 1 ]]; then
103
+ TARGET_NAME="${POSITIONAL[0]}"
104
+ fi
105
+ if [[ ${#POSITIONAL[@]} -ge 2 ]]; then
106
+ PUBLISH_JSON="${POSITIONAL[1]}"
107
+ fi
108
+ if [[ ${#POSITIONAL[@]} -gt 2 ]]; then
109
+ usage >&2
110
+ fail_pre_publish "unexpected extra positional arguments"
111
+ fi
112
+
113
+ # Silence unused warnings; these are contract flags for the adapter layer.
114
+ : "${TARGET_NAME:=}"
115
+ : "${NON_INTERACTIVE:=0}"
116
+
117
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
118
+ if REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)"; then
119
+ :
120
+ else
121
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
122
+ fi
123
+
124
+ VALIDATE_ENV_SCRIPT="$SCRIPT_DIR/validate_npm_env.sh"
125
+
126
+ command -v npm >/dev/null 2>&1 || fail_pre_publish "npm is required on PATH"
127
+
128
+ PACKAGE_JSON="$REPO_ROOT/package.json"
129
+ [[ -f "$PACKAGE_JSON" ]] || fail_pre_publish "package.json not found at '$PACKAGE_JSON'"
130
+
131
+ # Read a JSON string field. Prefers jq, falls back to node.
132
+ read_json_string() {
133
+ local file="$1"
134
+ local path="$2"
135
+ local value=""
136
+ if command -v jq >/dev/null 2>&1; then
137
+ value="$(jq -r "$path // \"\"" "$file" 2>/dev/null || true)"
138
+ elif command -v node >/dev/null 2>&1; then
139
+ # Convert a jq-style dot path (e.g. ".npm.dist_tag") to a JS accessor.
140
+ local js_path="${path#.}"
141
+ value="$(node -e "
142
+ try {
143
+ const data = require('$file');
144
+ const parts = '$js_path'.split('.').filter(Boolean);
145
+ let v = data;
146
+ for (const p of parts) { v = v == null ? undefined : v[p]; }
147
+ process.stdout.write(v == null ? '' : String(v));
148
+ } catch (e) { process.stdout.write(''); }
149
+ " 2>/dev/null || true)"
150
+ fi
151
+ printf '%s' "$value"
152
+ }
153
+
154
+ PACKAGE_NAME="$(read_json_string "$PACKAGE_JSON" '.name')"
155
+ PACKAGE_VERSION="$(read_json_string "$PACKAGE_JSON" '.version')"
156
+ [[ -n "$PACKAGE_NAME" ]] || fail_pre_publish "package.json is missing a 'name' field"
157
+ [[ -n "$PACKAGE_VERSION" ]] || fail_pre_publish "package.json is missing a 'version' field"
158
+
159
+ # Pull defaults from publish.json when both target + config are supplied.
160
+ CONFIG_DIST_TAG=""
161
+ CONFIG_REGISTRY=""
162
+ CONFIG_ACCESS=""
163
+ if [[ -n "$TARGET_NAME" && -n "$PUBLISH_JSON" ]]; then
164
+ [[ -f "$PUBLISH_JSON" ]] || fail_pre_publish "publish config not found at '$PUBLISH_JSON'"
165
+ command -v jq >/dev/null 2>&1 || fail_pre_publish "jq is required when passing a publish.json path"
166
+
167
+ TARGET_JSON="$(jq -c --arg target "$TARGET_NAME" \
168
+ '.targets[]? | select(.name == $target and .type == "npm")' \
169
+ "$PUBLISH_JSON")"
170
+ [[ -n "$TARGET_JSON" ]] \
171
+ || fail_pre_publish "npm target '$TARGET_NAME' was not found in '$PUBLISH_JSON'"
172
+
173
+ CONFIG_DIST_TAG="$(printf '%s' "$TARGET_JSON" | jq -r '.npm.dist_tag // ""')"
174
+ CONFIG_REGISTRY="$(printf '%s' "$TARGET_JSON" | jq -r '.npm.registry // ""')"
175
+ CONFIG_ACCESS="$(printf '%s' "$TARGET_JSON" | jq -r '.npm.access // ""')"
176
+
177
+ if [[ -f "$VALIDATE_ENV_SCRIPT" ]]; then
178
+ if ! bash "$VALIDATE_ENV_SCRIPT" "$PUBLISH_JSON" "$TARGET_NAME"; then
179
+ fail_pre_publish "environment validation via validate_npm_env.sh failed"
180
+ fi
181
+ else
182
+ log_warn "validate_npm_env.sh not found; skipping env pre-check"
183
+ fi
184
+ fi
185
+
186
+ # Resolve dist-tag with precedence: flag > env > config > default.
187
+ DIST_TAG=""
188
+ if [[ -n "$DIST_TAG_ARG" ]]; then
189
+ DIST_TAG="$DIST_TAG_ARG"
190
+ elif [[ -n "${NPM_DIST_TAG:-}" ]]; then
191
+ DIST_TAG="$NPM_DIST_TAG"
192
+ elif [[ -n "$CONFIG_DIST_TAG" ]]; then
193
+ DIST_TAG="$CONFIG_DIST_TAG"
194
+ else
195
+ DIST_TAG="latest"
196
+ fi
197
+
198
+ # Resolve registry: config value wins; otherwise npm's default.
199
+ if [[ -n "$CONFIG_REGISTRY" ]]; then
200
+ REGISTRY="$CONFIG_REGISTRY"
201
+ else
202
+ REGISTRY="https://registry.npmjs.org/"
203
+ fi
204
+
205
+ MODE_LABEL="live publish"
206
+ if (( DRY_RUN )); then
207
+ MODE_LABEL="dry-run"
208
+ fi
209
+
210
+ echo "========== NPM PUBLISH PIPELINE =========="
211
+ printf 'Package: %s\n' "$PACKAGE_NAME"
212
+ printf 'Version: %s\n' "$PACKAGE_VERSION"
213
+ printf 'Dist tag: %s\n' "$DIST_TAG"
214
+ printf 'Registry: %s\n' "$REGISTRY"
215
+ if [[ -n "$CONFIG_ACCESS" ]]; then
216
+ printf 'Access: %s\n' "$CONFIG_ACCESS"
217
+ fi
218
+ printf 'Mode: %s\n' "$MODE_LABEL"
219
+ printf 'Repo: %s\n' "$REPO_ROOT"
220
+ echo "=========================================="
221
+
222
+ PUBLISH_CMD=(npm publish --tag "$DIST_TAG")
223
+ if [[ -n "$CONFIG_ACCESS" ]]; then
224
+ PUBLISH_CMD+=(--access "$CONFIG_ACCESS")
225
+ fi
226
+ if [[ -n "$CONFIG_REGISTRY" ]]; then
227
+ PUBLISH_CMD+=(--registry "$CONFIG_REGISTRY")
228
+ fi
229
+
230
+ cd "$REPO_ROOT"
231
+
232
+ if (( DRY_RUN )); then
233
+ echo "DRY RUN — nothing was published"
234
+ PUBLISH_CMD+=(--dry-run)
235
+ log_info "Running: ${PUBLISH_CMD[*]}"
236
+ if "${PUBLISH_CMD[@]}"; then
237
+ log_info "Dry-run completed successfully."
238
+ exit 0
239
+ fi
240
+ fail_publish "npm publish --dry-run exited non-zero"
241
+ fi
242
+
243
+ log_info "Running: ${PUBLISH_CMD[*]}"
244
+ if "${PUBLISH_CMD[@]}"; then
245
+ log_info "Published $PACKAGE_NAME@$PACKAGE_VERSION to $REGISTRY (dist-tag: $DIST_TAG)"
246
+ exit 0
247
+ fi
248
+
249
+ fail_publish "npm publish exited non-zero"
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env bash
2
+
3
+ if [[ -n "${PUBLISH_COMMON_SH_LOADED:-}" ]]; then
4
+ return 0
5
+ fi
6
+ PUBLISH_COMMON_SH_LOADED=1
7
+
8
+ PUBLISH_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ if PUBLISH_REPO_ROOT="$(git -C "$PUBLISH_SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null)"; then
10
+ :
11
+ else
12
+ PUBLISH_REPO_ROOT="$(cd "$PUBLISH_SCRIPT_DIR/../../.." && pwd)"
13
+ fi
14
+ PUBLISH_DEFAULT_HISTORY_FILE="$PUBLISH_REPO_ROOT/project/logs/publish-history.json"
15
+ PUBLISH_DEFAULT_CONFIG_FILE="$PUBLISH_REPO_ROOT/project/configs/publish.json"
16
+
17
+ publish_stat_mtime() {
18
+ if [[ -e "$1" ]]; then
19
+ stat -f %m "$1" 2>/dev/null || echo 0
20
+ else
21
+ echo 0
22
+ fi
23
+ }
24
+
25
+ publish_acquire_lock() {
26
+ local lock_path="$1"
27
+ local attempt=0
28
+ local now mtime
29
+
30
+ while :; do
31
+ if [[ -e "$lock_path" ]]; then
32
+ now="$(date +%s)"
33
+ mtime="$(publish_stat_mtime "$lock_path")"
34
+ if (( now - mtime < 60 )); then
35
+ if (( attempt >= 1 )); then
36
+ return 1
37
+ fi
38
+ sleep 10
39
+ attempt=$((attempt + 1))
40
+ continue
41
+ fi
42
+ rm -f "$lock_path"
43
+ fi
44
+
45
+ if ( set -o noclobber; : > "$lock_path" ) 2>/dev/null; then
46
+ return 0
47
+ fi
48
+
49
+ if (( attempt >= 1 )); then
50
+ return 1
51
+ fi
52
+ sleep 10
53
+ attempt=$((attempt + 1))
54
+ done
55
+ }
56
+
57
+ publish_release_lock() {
58
+ rm -f "$1"
59
+ }
60
+
61
+ publish_normalize_path() {
62
+ local path="$1"
63
+ if [[ -z "$path" ]]; then
64
+ return 1
65
+ fi
66
+ if [[ "$path" = /* ]]; then
67
+ printf '%s\n' "$path"
68
+ else
69
+ printf '%s/%s\n' "$PUBLISH_REPO_ROOT" "$path"
70
+ fi
71
+ }
72
+
73
+ publish_resolve_history_file() {
74
+ local config_path="${1:-}"
75
+ local history_path="${PUBLISH_HISTORY_FILE:-}"
76
+
77
+ if [[ -z "$history_path" && -n "$config_path" && -f "$config_path" ]] && command -v jq >/dev/null 2>&1; then
78
+ history_path="$(jq -r '.defaults.history_file // empty' "$config_path" 2>/dev/null || true)"
79
+ fi
80
+
81
+ if [[ -z "$history_path" ]]; then
82
+ history_path="$PUBLISH_DEFAULT_HISTORY_FILE"
83
+ printf '%s\n' "$history_path"
84
+ return 0
85
+ fi
86
+
87
+ publish_normalize_path "$history_path"
88
+ }
89
+
90
+ publish_ensure_history_file() {
91
+ local history_file="$1"
92
+ local lock_file="${history_file}.lock"
93
+ local tmp_file="${history_file}.tmp.$$"
94
+
95
+ mkdir -p "$(dirname "$history_file")"
96
+ if ! publish_acquire_lock "$lock_file"; then
97
+ echo "Could not acquire lock for $history_file" >&2
98
+ return 1
99
+ fi
100
+
101
+ if [[ ! -f "$history_file" ]]; then
102
+ printf '[]\n' > "$history_file"
103
+ elif ! jq -e 'type == "array"' "$history_file" >/dev/null 2>&1; then
104
+ publish_release_lock "$lock_file"
105
+ rm -f "$tmp_file"
106
+ echo "Publish history file must contain a JSON array: $history_file" >&2
107
+ return 1
108
+ fi
109
+
110
+ publish_release_lock "$lock_file"
111
+ return 0
112
+ }
113
+
114
+ publish_append_history_entry() {
115
+ local history_file="$1"
116
+ local entry_json="$2"
117
+ local lock_file="${history_file}.lock"
118
+ local tmp_file="${history_file}.tmp.$$"
119
+
120
+ mkdir -p "$(dirname "$history_file")"
121
+ if ! publish_acquire_lock "$lock_file"; then
122
+ echo "Could not acquire lock for $history_file" >&2
123
+ return 1
124
+ fi
125
+
126
+ if [[ ! -f "$history_file" ]]; then
127
+ printf '[]\n' > "$history_file"
128
+ fi
129
+
130
+ if ! jq -e 'type == "array"' "$history_file" >/dev/null 2>&1; then
131
+ publish_release_lock "$lock_file"
132
+ rm -f "$tmp_file"
133
+ echo "Publish history file must contain a JSON array: $history_file" >&2
134
+ return 1
135
+ fi
136
+
137
+ if ! jq --argjson entry "$entry_json" '. + [$entry]' "$history_file" > "$tmp_file"; then
138
+ publish_release_lock "$lock_file"
139
+ rm -f "$tmp_file"
140
+ echo "Failed to append publish history entry." >&2
141
+ return 1
142
+ fi
143
+
144
+ mv "$tmp_file" "$history_file"
145
+ publish_release_lock "$lock_file"
146
+ return 0
147
+ }
148
+
149
+ publish_is_semver_tag() {
150
+ [[ "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
151
+ }
152
+
153
+ publish_list_semver_tags() {
154
+ local ref="${1:-HEAD}"
155
+ git tag --merged "$ref" --list 'v*.*.*' --sort=-v:refname 2>/dev/null | while IFS= read -r tag; do
156
+ [[ -z "$tag" ]] && continue
157
+ if publish_is_semver_tag "$tag"; then
158
+ printf '%s\n' "$tag"
159
+ fi
160
+ done
161
+ }
162
+
163
+ publish_history_has_version() {
164
+ local history_file="$1"
165
+ local version="$2"
166
+ [[ -f "$history_file" ]] || return 1
167
+ jq -e --arg version "$version" 'type == "array" and any(.[]?; (.version? // "") == $version)' "$history_file" >/dev/null 2>&1
168
+ }
169
+
170
+ publish_resolve_last_publish_tag() {
171
+ local history_file="$1"
172
+ local ref="${2:-HEAD}"
173
+ local tag
174
+
175
+ [[ -f "$history_file" ]] || return 1
176
+
177
+ while IFS= read -r tag; do
178
+ [[ -z "$tag" ]] && continue
179
+ if publish_history_has_version "$history_file" "$tag"; then
180
+ printf '%s\n' "$tag"
181
+ return 0
182
+ fi
183
+ done < <(publish_list_semver_tags "$ref")
184
+
185
+ return 1
186
+ }
187
+
188
+ publish_latest_ledger_version() {
189
+ local history_file="$1"
190
+ [[ -f "$history_file" ]] || return 1
191
+ jq -r '[.[]? | .version? | strings | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+$"))] | last // empty' "$history_file"
192
+ }
193
+
194
+ publish_normalize_version() {
195
+ local version="$1"
196
+ if [[ "$version" != v* ]]; then
197
+ version="v$version"
198
+ fi
199
+ publish_is_semver_tag "$version" || return 1
200
+ printf '%s\n' "$version"
201
+ }
202
+
203
+ publish_bump_version() {
204
+ local current_version="$1"
205
+ local bump_level="$2"
206
+ local major minor patch
207
+
208
+ current_version="$(publish_normalize_version "$current_version")" || return 1
209
+ IFS=. read -r major minor patch <<< "${current_version#v}"
210
+
211
+ case "$bump_level" in
212
+ major)
213
+ major=$((major + 1))
214
+ minor=0
215
+ patch=0
216
+ ;;
217
+ minor)
218
+ minor=$((minor + 1))
219
+ patch=0
220
+ ;;
221
+ patch)
222
+ patch=$((patch + 1))
223
+ ;;
224
+ *)
225
+ return 1
226
+ ;;
227
+ esac
228
+
229
+ printf 'v%s.%s.%s\n' "$major" "$minor" "$patch"
230
+ }
231
+
232
+ publish_current_actor() {
233
+ local actor
234
+ actor="$(git config user.name 2>/dev/null || true)"
235
+ if [[ -n "$actor" ]]; then
236
+ printf '%s\n' "$actor"
237
+ return 0
238
+ fi
239
+
240
+ actor="${GITHUB_ACTOR:-${USER:-}}"
241
+ if [[ -n "$actor" ]]; then
242
+ printf '%s\n' "$actor"
243
+ return 0
244
+ fi
245
+
246
+ printf 'CI\n'
247
+ }
248
+
249
+ publish_temp_release_notes_path() {
250
+ local dir="$PUBLISH_REPO_ROOT/.claude/publish-tmp"
251
+ mkdir -p "$dir"
252
+ printf '%s/release-notes-%s.md\n' "$dir" "$(date -u +"%Y%m%dT%H%M%SZ")"
253
+ }