forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6

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 (196) hide show
  1. package/AGENTS.md +18 -7
  2. package/CHANGELOG.md +79 -1
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/README.md +6 -2
  6. package/bin/forge-cmd.js +20 -0
  7. package/bin/forge.js +28 -375
  8. package/docs/INDEX.md +1 -1
  9. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  10. package/docs/guides/MIGRATION.md +4 -4
  11. package/docs/guides/SETUP.md +16 -16
  12. package/docs/reference/COMMANDS.md +8 -5
  13. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  14. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  15. package/docs/reference/INSTALL.md +4 -0
  16. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  17. package/docs/reference/RELEASE.md +5 -3
  18. package/docs/reference/TOOLCHAIN.md +8 -0
  19. package/docs/reference/github-accounts.md +134 -0
  20. package/docs/reference/protected-state-surfaces.md +4 -4
  21. package/docs/reference/shepherd.md +114 -35
  22. package/lefthook.yml +12 -0
  23. package/lib/activation/ensure-forge-home.js +33 -15
  24. package/lib/adapters/pr-state-adapter.js +359 -144
  25. package/lib/audit-evidence.js +71 -110
  26. package/lib/base-remote.js +138 -0
  27. package/lib/beta5-compatibility-evidence.js +1093 -0
  28. package/lib/bun-lockfile-proof.js +413 -0
  29. package/lib/bun-workflow-pins.js +461 -0
  30. package/lib/capabilities/index.js +9 -0
  31. package/lib/capabilities/model.js +141 -0
  32. package/lib/capabilities/probes.js +347 -0
  33. package/lib/capped-jsonl-log.js +236 -0
  34. package/lib/codex-skills.js +2 -2
  35. package/lib/commands/_manifest.js +1 -0
  36. package/lib/commands/_registry.js +50 -20
  37. package/lib/commands/clean.js +252 -32
  38. package/lib/commands/dev.js +4 -33
  39. package/lib/commands/doctor.js +37 -6
  40. package/lib/commands/gate.js +197 -27
  41. package/lib/commands/github.js +215 -0
  42. package/lib/commands/hooks.js +276 -30
  43. package/lib/commands/insights.js +8 -3
  44. package/lib/commands/memory.js +66 -2
  45. package/lib/commands/merge.js +1265 -58
  46. package/lib/commands/plan.js +33 -2
  47. package/lib/commands/pr.js +3 -1
  48. package/lib/commands/preflight.js +21 -4
  49. package/lib/commands/prime.js +21 -8
  50. package/lib/commands/push.js +146 -54
  51. package/lib/commands/recall.js +127 -49
  52. package/lib/commands/recap.js +6 -1
  53. package/lib/commands/release.js +39 -3
  54. package/lib/commands/remember.js +28 -4
  55. package/lib/commands/serve.js +26 -9
  56. package/lib/commands/setup.js +323 -98
  57. package/lib/commands/shepherd.js +591 -73
  58. package/lib/commands/ship.js +36 -91
  59. package/lib/commands/skill.js +127 -11
  60. package/lib/commands/status.js +17 -1
  61. package/lib/commands/team.js +47 -8
  62. package/lib/commands/test.js +187 -38
  63. package/lib/commands/validate.js +65 -21
  64. package/lib/commands/worktree.js +359 -45
  65. package/lib/core/runtime-graph.js +1 -1
  66. package/lib/doc-assertions.js +297 -0
  67. package/lib/existing-tdd-gate.js +253 -0
  68. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  69. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  70. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  71. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  72. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  73. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  74. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  75. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  76. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  77. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  78. package/lib/forge-context.js +1 -4
  79. package/lib/forge-issues.js +134 -32
  80. package/lib/gate-events.js +98 -10
  81. package/lib/git-defaults.js +56 -0
  82. package/lib/github-context.js +308 -0
  83. package/lib/global-flags.js +1 -0
  84. package/lib/harness-capability-matrix.js +3 -3
  85. package/lib/hook-renderer.js +122 -5
  86. package/lib/insights.js +96 -80
  87. package/lib/issue-render.js +19 -0
  88. package/lib/kernel/backing-issue.js +14 -2
  89. package/lib/kernel/broker.js +739 -31
  90. package/lib/kernel/claim-reconciler.js +238 -0
  91. package/lib/kernel/cli-broker-factory.js +12 -1
  92. package/lib/kernel/close-on-merge.js +154 -0
  93. package/lib/kernel/fs-class.js +42 -25
  94. package/lib/kernel/lease-enforcer.js +9 -4
  95. package/lib/kernel/legacy-claim-repair.js +442 -0
  96. package/lib/kernel/live-claim-projection.js +26 -0
  97. package/lib/kernel/migrations.js +118 -3
  98. package/lib/kernel/readiness-model.js +184 -12
  99. package/lib/kernel/schema.js +49 -1
  100. package/lib/kernel/sqlite-driver.js +3435 -172
  101. package/lib/kernel/taxonomy-validator.js +4 -1
  102. package/lib/kernel/windows-private-acl.js +239 -0
  103. package/lib/lefthook-wiring.js +21 -1
  104. package/lib/memory/hygiene.js +191 -0
  105. package/lib/memory/router.js +110 -28
  106. package/lib/memory/usage-evidence.js +4 -0
  107. package/lib/memory-digest.js +106 -15
  108. package/lib/memory-recall-events.js +145 -0
  109. package/lib/memory-recall.js +71 -10
  110. package/lib/merge-rules.js +143 -21
  111. package/lib/npm-publish-workflow.js +465 -0
  112. package/lib/orientation.js +68 -43
  113. package/lib/package-root.js +2 -0
  114. package/lib/plugin-catalog.js +14 -4
  115. package/lib/pr-bundle.js +5 -6
  116. package/lib/pr-monitor/auto-actions.js +169 -28
  117. package/lib/pr-monitor/differ.js +110 -4
  118. package/lib/pr-monitor/events.js +0 -0
  119. package/lib/pr-monitor/flow-monitor.js +1424 -0
  120. package/lib/pr-monitor/gather.js +251 -44
  121. package/lib/pr-monitor/journal.js +18 -39
  122. package/lib/pr-monitor/monitor.js +117 -10
  123. package/lib/pr-monitor/process-identity.js +117 -0
  124. package/lib/pr-monitor/reconcile-executor.js +1129 -470
  125. package/lib/pr-monitor/reconcile.js +0 -0
  126. package/lib/pr-monitor/render-summary.js +293 -0
  127. package/lib/pr-monitor/review-preflight.js +269 -0
  128. package/lib/pr-monitor/shepherd-lease.js +38 -20
  129. package/lib/pr-monitor/verdict.js +438 -0
  130. package/lib/pr-monitor/watch-lifecycle.js +145 -27
  131. package/lib/pr-monitor/watch-owner.js +1414 -0
  132. package/lib/pr-monitor/watch.js +129 -58
  133. package/lib/pr-pull.js +33 -14
  134. package/lib/pr-shepherd.js +51 -11
  135. package/lib/preflight/gates.js +65 -18
  136. package/lib/preflight/runner.js +5 -0
  137. package/lib/project-memory.js +178 -4
  138. package/lib/protected-state-authority.js +1100 -0
  139. package/lib/protected-state-surfaces.js +243 -45
  140. package/lib/release-readiness.js +53 -7
  141. package/lib/review-adapter.js +65 -0
  142. package/lib/shell-utils.js +1 -1
  143. package/lib/skills-sync.js +71 -35
  144. package/lib/smart-merge.js +28 -4
  145. package/lib/symlink-utils.js +74 -26
  146. package/lib/upgrade-safety.js +39 -0
  147. package/lib/using-forge.js +19 -6
  148. package/lib/validation/risk-manifest.js +339 -0
  149. package/lib/workflow/enforce-stage.js +44 -0
  150. package/lib/workflow/plan-authority.js +225 -0
  151. package/package.json +12 -9
  152. package/scripts/commitlint.js +13 -15
  153. package/scripts/doc-asserting-tests.js +158 -0
  154. package/scripts/generate-risk-manifest.js +91 -0
  155. package/scripts/github-context-bridge.sh +10 -0
  156. package/scripts/legacy-claim-repair.js +145 -0
  157. package/scripts/lib/behavioral-eval-runner.js +310 -0
  158. package/scripts/lib/behavioral-eval-runtime.js +457 -0
  159. package/scripts/lib/eval-evidence.js +328 -0
  160. package/scripts/lib/eval-runner.js +81 -41
  161. package/scripts/lib/immutable-eval-corpus.js +309 -0
  162. package/scripts/lib/promotion-evidence-loader.js +94 -0
  163. package/scripts/lib/promotion-scorecard.js +314 -0
  164. package/scripts/npm-release-receipt.js +134 -0
  165. package/scripts/process-tree.js +773 -0
  166. package/scripts/protected-state-check.js +479 -31
  167. package/scripts/run-command-eval.js +29 -1
  168. package/scripts/sync-agent-skills.js +333 -34
  169. package/scripts/sync-d20-audit.js +172 -0
  170. package/scripts/test-full-suite.js +935 -37
  171. package/scripts/test-profile.js +13 -3
  172. package/scripts/test.js +271 -57
  173. package/skills/coverage.json +1 -0
  174. package/skills/review/SKILL.md +6 -11
  175. package/skills/review/evals/scorecard.json +4 -4
  176. package/skills/rollback/SKILL.md +4 -11
  177. package/skills/rollback/evals/scorecard.json +3 -3
  178. package/skills/setup/SKILL.md +18 -0
  179. package/skills/setup/evals/scorecard.json +3 -3
  180. package/skills/shepherd/SKILL.md +39 -16
  181. package/skills/shepherd/evals/scorecard.json +4 -4
  182. package/skills/ship/SKILL.md +4 -12
  183. package/skills/ship/evals/scorecard.json +3 -3
  184. package/skills/validate/SKILL.md +3 -0
  185. package/skills/validate/evals/scorecard.json +1 -1
  186. package/skills/worktree/SKILL.md +6 -1
  187. package/skills/worktree/evals/scorecard.json +2 -2
  188. package/lib/beads-setup.js +0 -538
  189. package/lib/beads-sync-scaffold.js +0 -189
  190. package/lib/pat-setup.js +0 -207
  191. package/lib/pr-monitor/render-sticky.js +0 -206
  192. package/lib/pr-monitor/upsert-sticky.js +0 -169
  193. package/scripts/beads-context.sh +0 -577
  194. package/scripts/beads-migrate-to-dolt.sh +0 -7
  195. package/scripts/beads-upgrade-smoke.sh +0 -284
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -1,577 +0,0 @@
1
- #!/usr/bin/env bash
2
- # beads-context.sh — Helper script to manage issue context
3
- # for the Forge 7-stage workflow.
4
- #
5
- # Subcommands:
6
- # set-design <issue-id> <task-count> <task-file-path>
7
- # set-acceptance <issue-id> "<criteria-text>"
8
- # update-progress <issue-id> <task-num> <total> "<title>" <commit-sha> <test-count> <gate-count>
9
- # parse-progress <issue-id>
10
- # stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."] [--workflow-state "{...}"]
11
- # validate <issue-id>
12
- #
13
- # Cross-platform: works on Windows (Git Bash), macOS, and Linux.
14
- # OWASP A03: All variables properly quoted to prevent shell injection.
15
-
16
- set -euo pipefail
17
-
18
- # Source shared sanitize library
19
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
- if [[ -f "$SCRIPT_DIR/bootstrap-windows-tools.sh" ]]; then
21
- source "$SCRIPT_DIR/bootstrap-windows-tools.sh"
22
- fi
23
- source "$SCRIPT_DIR/lib/sanitize.sh"
24
-
25
- # ── Helpers ──────────────────────────────────────────────────────────────
26
-
27
- usage() {
28
- cat >&2 <<'EOF'
29
- Usage: beads-context.sh <subcommand> [args...]
30
-
31
- Subcommands:
32
- set-design <issue-id> <task-count> <task-file-path>
33
- set-acceptance <issue-id> "<criteria-text>"
34
- update-progress <issue-id> <task-num> <total> "<title>" <commit-sha> <test-count> <gate-count>
35
- parse-progress <issue-id>
36
- stage-transition <issue-id> <completed-stage> <next-stage> [--summary "..."] [--decisions "..."] [--artifacts "..."] [--next "..."] [--workflow-state "{...}"]
37
- validate <issue-id>
38
- EOF
39
- exit 1
40
- }
41
-
42
- die() {
43
- echo "Error: $1" >&2
44
- exit 1
45
- }
46
-
47
- # Resolve a Windows forge.exe installation for bash-based helper flows.
48
- # This covers WSL/Git Bash cases where PowerShell can run forge.exe but bash PATH
49
- # does not include the Windows install directory.
50
- convert_windows_path() {
51
- local raw="${1%$'\r'}"
52
-
53
- if [[ -z "$raw" ]]; then
54
- return 1
55
- fi
56
-
57
- if [[ ! "$raw" =~ ^[A-Za-z]:\\ ]]; then
58
- printf '%s' "$raw"
59
- return 0
60
- fi
61
-
62
- if command -v wslpath >/dev/null 2>&1; then
63
- wslpath -u "$raw" 2>/dev/null && return 0
64
- fi
65
-
66
- if command -v cygpath >/dev/null 2>&1; then
67
- cygpath -u "$raw" 2>/dev/null && return 0
68
- fi
69
-
70
- local drive rest
71
- drive="$(printf '%s' "$raw" | cut -c1 | tr '[:upper:]' '[:lower:]')"
72
- rest="${raw:2}"
73
- rest="${rest//\\//}"
74
- printf '/mnt/%s%s' "$drive" "$rest"
75
- }
76
-
77
- is_runnable_forge_candidate() {
78
- local candidate="${1:-}"
79
-
80
- if [[ -z "$candidate" || ! -f "$candidate" ]]; then
81
- return 1
82
- fi
83
-
84
- [[ -x "$candidate" || "$candidate" == *.exe ]]
85
- }
86
-
87
- resolve_forge_cmd() {
88
- local candidate=""
89
- local converted=""
90
-
91
- if [[ -n "${FORGE_CMD:-}" ]]; then
92
- if [[ "${FORGE_CMD}" == *"/"* || "${FORGE_CMD}" == *"\\"* ]]; then
93
- converted="$(convert_windows_path "$FORGE_CMD")"
94
- if is_runnable_forge_candidate "$converted"; then
95
- printf '%s' "$converted"
96
- return 0
97
- fi
98
-
99
- is_runnable_forge_candidate "$FORGE_CMD" || return 1
100
- fi
101
- printf '%s' "$FORGE_CMD"
102
- return 0
103
- fi
104
-
105
- if command -v forge >/dev/null 2>&1; then
106
- printf '%s' "forge"
107
- return 0
108
- fi
109
-
110
- if command -v forge.exe >/dev/null 2>&1; then
111
- printf '%s' "forge.exe"
112
- return 0
113
- fi
114
-
115
- for candidate in \
116
- "$HOME/.local/bin/forge" \
117
- "$HOME/.local/bin/forge.exe" \
118
- "$HOME/.bun/bin/forge" \
119
- "$HOME/.bun/bin/forge.exe"
120
- do
121
- if is_runnable_forge_candidate "$candidate"; then
122
- printf '%s' "$candidate"
123
- return 0
124
- fi
125
- done
126
-
127
- if command -v where.exe >/dev/null 2>&1; then
128
- while IFS= read -r candidate; do
129
- candidate="${candidate%$'\r'}"
130
- [[ -z "$candidate" ]] && continue
131
-
132
- converted="$(convert_windows_path "$candidate")"
133
- if is_runnable_forge_candidate "$converted"; then
134
- printf '%s' "$converted"
135
- return 0
136
- fi
137
-
138
- if is_runnable_forge_candidate "$candidate"; then
139
- printf '%s' "$candidate"
140
- return 0
141
- fi
142
- done < <(where.exe forge 2>/dev/null || true)
143
- fi
144
-
145
- return 1
146
- }
147
-
148
- FORGE=""
149
-
150
- get_forge_cmd() {
151
- if [[ -z "$FORGE" ]]; then
152
- FORGE="$(resolve_forge_cmd)" || die "forge is required but not found"
153
- fi
154
-
155
- printf '%s' "$FORGE"
156
- }
157
-
158
- # Run forge issue update and check for errors in both exit code and output.
159
- # The update operation exits 0 even for non-existent issues, so we check stdout too.
160
- forge_update() {
161
- local forge_cmd
162
- forge_cmd="$(get_forge_cmd)"
163
- local output
164
- output="$("$forge_cmd" issue update "$@" 2>&1)"
165
- local rc=$?
166
-
167
- if [[ $rc -ne 0 ]]; then
168
- echo "$output" >&2
169
- return 1
170
- fi
171
-
172
- # Forge prints "Error resolving/updating ..." to stdout for non-existent issues
173
- # Use specific patterns to avoid false positives from data containing "error"
174
- if printf '%s' "$output" | grep -Eqi '^Error|Error resolving|Error updating'; then
175
- echo "$output" >&2
176
- return 1
177
- fi
178
-
179
- echo "$output"
180
- return 0
181
- }
182
-
183
- # Run forge issue comment and check for errors similarly.
184
- forge_comment() {
185
- local forge_cmd
186
- forge_cmd="$(get_forge_cmd)"
187
- local output
188
- output="$("$forge_cmd" issue comment "$@" 2>&1)"
189
- local rc=$?
190
-
191
- if [[ $rc -ne 0 ]]; then
192
- echo "$output" >&2
193
- return 1
194
- fi
195
-
196
- # Use specific error patterns to avoid false positives from data containing "error"
197
- if printf '%s' "$output" | grep -Eqi '^Error|Error resolving|Error adding'; then
198
- echo "$output" >&2
199
- return 1
200
- fi
201
-
202
- echo "$output"
203
- return 0
204
- }
205
-
206
- # Extract concatenated comment text from a `forge issue show --json` payload.
207
- # Forge/issue show returns comments as an array of objects ({text:...}) — matching
208
- # the issue-state contract in lib/workflow/state-manager.js — or, defensively, a
209
- # raw string. jq is preferred; a grep/sed fallback captures "text" fields.
210
- extract_comments_text() {
211
- local json="$1"
212
-
213
- if command -v jq &>/dev/null; then
214
- printf '%s' "$json" | jq -r '
215
- ((if type == "array" then .[0] else . end).comments // [])
216
- | if type == "array" then (map(.text? // tostring) | join("\n"))
217
- else tostring end
218
- ' 2>/dev/null || true
219
- return 0
220
- fi
221
-
222
- printf '%s' "$json" | grep -o '"text": *"[^"]*"' | sed 's/^"text": *"//;s/"$//' || true
223
- }
224
-
225
- # ── Subcommands ──────────────────────────────────────────────────────────
226
-
227
- cmd_set_design() {
228
- if [[ $# -lt 3 ]]; then
229
- echo "Usage: beads-context.sh set-design <issue-id> <task-count> <task-file-path>" >&2
230
- exit 1
231
- fi
232
-
233
- local issue_id="$1"
234
- local task_count
235
- task_count="$(sanitize "$2")"
236
- local task_file
237
- task_file="$(sanitize "$3")"
238
-
239
- local design_text="${task_count} tasks | ${task_file}"
240
-
241
- if ! forge_update "$issue_id" --design "$design_text" > /dev/null; then
242
- die "Failed to set design on issue ${issue_id}"
243
- fi
244
-
245
- echo "Design set on ${issue_id}: ${design_text}"
246
- }
247
-
248
- cmd_set_acceptance() {
249
- if [[ $# -lt 2 ]]; then
250
- echo "Usage: beads-context.sh set-acceptance <issue-id> \"<criteria-text>\"" >&2
251
- exit 1
252
- fi
253
-
254
- local issue_id="$1"
255
- local criteria
256
- criteria="$(sanitize "$2")"
257
-
258
- if ! forge_update "$issue_id" --acceptance "$criteria" > /dev/null; then
259
- die "Failed to set acceptance on issue ${issue_id}"
260
- fi
261
-
262
- echo "Acceptance criteria set on ${issue_id}"
263
- }
264
-
265
- cmd_update_progress() {
266
- if [[ $# -lt 7 ]]; then
267
- echo "Usage: beads-context.sh update-progress <issue-id> <task-num> <total> \"<title>\" <commit-sha> <test-count> <gate-count>" >&2
268
- exit 1
269
- fi
270
-
271
- local issue_id="$1"
272
- local task_num
273
- task_num="$(sanitize "$2")"
274
- local total
275
- total="$(sanitize "$3")"
276
- local title
277
- title="$(sanitize "$4")"
278
- local commit_sha
279
- commit_sha="$(sanitize "$5")"
280
- local test_count
281
- test_count="$(sanitize "$6")"
282
- local gate_count
283
- gate_count="$(sanitize "$7")"
284
-
285
- # Strip backslashes from progress fields only — these are read back via
286
- # printf '%b' in parse-progress, where \n/\t would expand into real escapes.
287
- # Other commands (set-design, set-acceptance, stage-transition) keep backslashes
288
- # intact to preserve Windows-style paths like docs\plans\tasks.md.
289
- task_num="${task_num//\\/}"
290
- total="${total//\\/}"
291
- title="${title//\\/}"
292
- commit_sha="${commit_sha//\\/}"
293
- test_count="${test_count//\\/}"
294
- gate_count="${gate_count//\\/}"
295
-
296
- local note="Task ${task_num}/${total} done: ${title} | ${test_count} tests | ${commit_sha} | ${gate_count} gates"
297
-
298
- if ! forge_update "$issue_id" --append-notes "$note" > /dev/null; then
299
- die "Failed to update progress on issue ${issue_id}"
300
- fi
301
-
302
- echo "Progress updated on ${issue_id}: Task ${task_num}/${total}"
303
- }
304
-
305
- cmd_parse_progress() {
306
- if [[ $# -lt 1 ]]; then
307
- echo "Usage: beads-context.sh parse-progress <issue-id>" >&2
308
- exit 1
309
- fi
310
-
311
- local issue_id="$1"
312
- local forge_cmd
313
- forge_cmd="$(get_forge_cmd)"
314
-
315
- # Get the issue JSON — detect non-existent issues
316
- local json
317
- json="$("$forge_cmd" issue show "$issue_id" --json 2>&1)" || die "Failed to show issue ${issue_id}"
318
-
319
- # show may exit 0 but print an error for non-existent issues
320
- # Match specific error patterns, not the word "error" in data fields
321
- if printf '%s' "$json" | grep -Eqi '^Error (fetching|resolving)'; then
322
- die "Issue not found: ${issue_id}"
323
- fi
324
-
325
- # show returns "[]" or empty for non-existent issues
326
- if [[ -z "$json" || "$json" == "[]" || "$json" == "null" ]]; then
327
- die "Issue not found: ${issue_id}"
328
- fi
329
-
330
- # Extract notes field from JSON — prefer jq if available, fall back to grep/sed
331
- local notes
332
- if command -v jq &>/dev/null; then
333
- # Handle both array ([{...}]) and object ({...}) show responses
334
- notes="$(printf '%s' "$json" | jq -r 'if type == "array" then .[0].notes else .notes end // empty' 2>/dev/null || true)"
335
- else
336
- notes="$(printf '%s' "$json" | grep -o '"notes": *"[^"]*"' | head -1 | sed 's/^"notes": *"//;s/"$//' || true)"
337
- fi
338
-
339
- if [[ -z "$notes" ]]; then
340
- echo "No progress data"
341
- return 0
342
- fi
343
-
344
- # Split on literal \n (two chars: backslash + n) and count task lines
345
- # Use printf to expand the \n into actual newlines for grep (POSIX-compliant)
346
- local expanded
347
- expanded="$(printf '%b' "$notes")"
348
-
349
- local task_lines
350
- task_lines="$(echo "$expanded" | grep -c 'Task [0-9]*/[0-9]* done:' || true)"
351
-
352
- if [[ "$task_lines" -eq 0 ]]; then
353
- echo "No progress data"
354
- return 0
355
- fi
356
-
357
- # Get the last task line
358
- local last_line
359
- last_line="$(echo "$expanded" | grep 'Task [0-9]*/[0-9]* done:' | tail -1 || true)"
360
-
361
- # Extract total (N/M) from last line
362
- local total
363
- total="$(echo "$last_line" | grep -o '[0-9]*/[0-9]*' | head -1 | cut -d/ -f2)"
364
-
365
- # Extract last task title (between "done: " and " |")
366
- local last_title
367
- last_title="$(echo "$last_line" | sed 's/.*done: //;s/ |.*//')"
368
-
369
- # Extract commit sha — 4th pipe-separated field
370
- # Format: "Task N/M done: <title> | <test_count> tests | <sha> | <gate_count> gates"
371
- local last_sha
372
- # Use [|] character class for portable literal pipe matching across gawk and BSD awk
373
- last_sha="$(echo "$last_line" | awk -F ' [|] ' '{print $3}' | sed 's/^ *//;s/ *$//')"
374
-
375
- echo "${task_lines}/${total} tasks done | Last: ${last_title} (${last_sha})"
376
- }
377
-
378
- cmd_stage_transition() {
379
- if [[ $# -lt 3 ]]; then
380
- echo "Usage: beads-context.sh stage-transition <issue-id> <completed-stage> <next-stage> [--summary \"...\"] [--decisions \"...\"] [--artifacts \"...\"] [--next \"...\"] [--workflow-state \"{...}\"]" >&2
381
- exit 1
382
- fi
383
-
384
- local issue_id="$1"
385
- local completed
386
- completed="$(sanitize "$2")"
387
- local next
388
- next="$(sanitize "$3")"
389
- shift 3
390
-
391
- # Parse optional flags
392
- local flag_summary="" flag_decisions="" flag_artifacts="" flag_next="" flag_workflow_state=""
393
- while [[ $# -gt 0 ]]; do
394
- case "$1" in
395
- --summary)
396
- shift
397
- flag_summary="$(sanitize "${1:-}")"
398
- ;;
399
- --decisions)
400
- shift
401
- flag_decisions="$(sanitize "${1:-}")"
402
- ;;
403
- --artifacts)
404
- shift
405
- flag_artifacts="$(sanitize "${1:-}")"
406
- ;;
407
- --next)
408
- shift
409
- flag_next="$(sanitize "${1:-}")"
410
- ;;
411
- --workflow-state)
412
- shift
413
- flag_workflow_state="$(sanitize_config_value "${1:-}")"
414
- ;;
415
- *)
416
- # Ignore unknown flags for forward compatibility
417
- ;;
418
- esac
419
- shift
420
- done
421
-
422
- # Build comment: header line always present
423
- local comment="Stage: ${completed} complete → ready for ${next}"
424
-
425
- # Append structured fields only if provided
426
- if [[ -n "$flag_summary" ]]; then
427
- comment="${comment}
428
- Summary: ${flag_summary}"
429
- fi
430
- if [[ -n "$flag_decisions" ]]; then
431
- comment="${comment}
432
- Decisions: ${flag_decisions}"
433
- fi
434
- if [[ -n "$flag_artifacts" ]]; then
435
- comment="${comment}
436
- Artifacts: ${flag_artifacts}"
437
- fi
438
- if [[ -n "$flag_next" ]]; then
439
- comment="${comment}
440
- Next: ${flag_next}"
441
- fi
442
- if [[ -n "$flag_workflow_state" ]]; then
443
- comment="${comment}
444
- WorkflowState: ${flag_workflow_state}"
445
- fi
446
-
447
- if ! forge_comment "$issue_id" "$comment" > /dev/null; then
448
- die "Failed to record stage transition on issue ${issue_id}"
449
- fi
450
-
451
- echo "Stage transition recorded on ${issue_id}: ${completed} → ${next}"
452
- }
453
-
454
- cmd_validate() {
455
- if [[ $# -lt 1 ]]; then
456
- echo "Usage: beads-context.sh validate <issue-id>" >&2
457
- exit 1
458
- fi
459
-
460
- local issue_id="$1"
461
- local warnings=0
462
- local forge_cmd
463
- forge_cmd="$(get_forge_cmd)"
464
-
465
- # Get issue JSON
466
- local json
467
- json="$("$forge_cmd" issue show "$issue_id" --json 2>&1)" || {
468
- echo "Error: Failed to retrieve issue ${issue_id}" >&2
469
- exit 1
470
- }
471
-
472
- # Check for error patterns
473
- if printf '%s' "$json" | grep -Eqi '^Error|Error resolving|Error fetching'; then
474
- echo "Error: Issue not found: ${issue_id}" >&2
475
- exit 1
476
- fi
477
-
478
- if [[ -z "$json" || "$json" == "[]" || "$json" == "null" ]]; then
479
- echo "Error: Issue not found: ${issue_id}" >&2
480
- exit 1
481
- fi
482
-
483
- # Extract fields — prefer jq, fall back to grep/sed
484
- local description="" design=""
485
- if command -v jq &>/dev/null; then
486
- description="$(printf '%s' "$json" | jq -r 'if type == "array" then .[0].description else .description end // empty' 2>/dev/null || true)"
487
- design="$(printf '%s' "$json" | jq -r 'if type == "array" then .[0].design else .design end // empty' 2>/dev/null || true)"
488
- else
489
- description="$(printf '%s' "$json" | grep -o '"description": *"[^"]*"' | head -1 | sed 's/^"description": *"//;s/"$//' || true)"
490
- design="$(printf '%s' "$json" | grep -o '"design": *"[^"]*"' | head -1 | sed 's/^"design": *"//;s/"$//' || true)"
491
- fi
492
-
493
- # Check 1: Issue has description
494
- if [[ -z "$description" ]]; then
495
- echo "Warning: Issue ${issue_id} has no description"
496
- warnings=$((warnings + 1))
497
- fi
498
-
499
- # The issue payload returned by `forge issue show --json` already includes its
500
- # comment history, so the stage-transition checks read comments from that single
501
- # response rather than issuing a separate comment-list query.
502
- local comments
503
- comments="$(extract_comments_text "$json")"
504
-
505
- # Check 2: At least one stage transition exists
506
- local has_transition=false
507
- if echo "$comments" | grep -q 'Stage:.*complete'; then
508
- has_transition=true
509
- fi
510
-
511
- if [[ "$has_transition" == "false" ]]; then
512
- echo "Warning: No stage transition found on issue ${issue_id}"
513
- warnings=$((warnings + 1))
514
- fi
515
-
516
- # Check 3: Most recent transition has summary
517
- # Isolate the last transition block to avoid false-positives from earlier summaries
518
- if [[ "$has_transition" == "true" ]]; then
519
- local has_summary=false
520
- local last_block
521
- last_block="$(echo "$comments" | awk '/Stage:.*complete/{found=1; block=""} found{block=block"\n"$0} END{print block}')"
522
- if echo "$last_block" | grep -q 'Summary:'; then
523
- has_summary=true
524
- fi
525
- if [[ "$has_summary" == "false" ]]; then
526
- echo "Warning: Most recent stage transition has no summary on issue ${issue_id}"
527
- warnings=$((warnings + 1))
528
- fi
529
- fi
530
-
531
- # Check 4: Design metadata is set if past plan stage
532
- # "Past plan" means there's a transition FROM plan or any later stage
533
- local past_plan=false
534
- if echo "$comments" | grep -q 'Stage: plan complete'; then
535
- past_plan=true
536
- fi
537
- if echo "$comments" | grep -Eq 'Stage: (dev|validate|ship|review|premerge) complete'; then
538
- past_plan=true
539
- fi
540
-
541
- if [[ "$past_plan" == "true" && -z "$design" ]]; then
542
- echo "Warning: Design metadata not set on issue ${issue_id} (past plan stage)"
543
- warnings=$((warnings + 1))
544
- fi
545
-
546
- # Summary
547
- if [[ $warnings -eq 0 ]]; then
548
- echo "All context fields present on issue ${issue_id}"
549
- else
550
- echo "${warnings} warning(s) found on issue ${issue_id}"
551
- fi
552
-
553
- # Always exit 0 — advisory only
554
- exit 0
555
- }
556
-
557
- # ── Main dispatcher ──────────────────────────────────────────────────────
558
-
559
- if [[ $# -lt 1 ]]; then
560
- usage
561
- fi
562
-
563
- subcommand="$1"
564
- shift
565
-
566
- case "$subcommand" in
567
- set-design) cmd_set_design "$@" ;;
568
- set-acceptance) cmd_set_acceptance "$@" ;;
569
- update-progress) cmd_update_progress "$@" ;;
570
- parse-progress) cmd_parse_progress "$@" ;;
571
- stage-transition) cmd_stage_transition "$@" ;;
572
- validate) cmd_validate "$@" ;;
573
- *)
574
- echo "Error: Unknown subcommand '${subcommand}'" >&2
575
- usage
576
- ;;
577
- esac
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- NODE_CMD="${NODE_CMD:-node}"
6
-
7
- exec "$NODE_CMD" "$SCRIPT_DIR/lib/beads-migrate-to-dolt.mjs" "$@"