specweave 1.0.235 → 1.0.239

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/README.md +89 -193
  2. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts +37 -0
  3. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.d.ts.map +1 -0
  4. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js +176 -0
  5. package/dist/plugins/specweave-github/lib/github-ac-comment-poster.js.map +1 -0
  6. package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts +36 -0
  7. package/dist/plugins/specweave-github/lib/github-batch-sync.d.ts.map +1 -0
  8. package/dist/plugins/specweave-github/lib/github-batch-sync.js +115 -0
  9. package/dist/plugins/specweave-github/lib/github-batch-sync.js.map +1 -0
  10. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts +37 -0
  11. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.d.ts.map +1 -0
  12. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js +56 -0
  13. package/dist/plugins/specweave-github/lib/github-board-resolver-v2.js.map +1 -0
  14. package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts +68 -0
  15. package/dist/plugins/specweave-github/lib/github-conflict-resolver.d.ts.map +1 -0
  16. package/dist/plugins/specweave-github/lib/github-conflict-resolver.js +102 -0
  17. package/dist/plugins/specweave-github/lib/github-conflict-resolver.js.map +1 -0
  18. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts +64 -0
  19. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.d.ts.map +1 -0
  20. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js +162 -0
  21. package/dist/plugins/specweave-github/lib/github-cross-repo-sync.js.map +1 -0
  22. package/dist/plugins/specweave-github/lib/github-field-sync.d.ts +50 -0
  23. package/dist/plugins/specweave-github/lib/github-field-sync.d.ts.map +1 -0
  24. package/dist/plugins/specweave-github/lib/github-field-sync.js +107 -0
  25. package/dist/plugins/specweave-github/lib/github-field-sync.js.map +1 -0
  26. package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts +53 -0
  27. package/dist/plugins/specweave-github/lib/github-graphql-client.d.ts.map +1 -0
  28. package/dist/plugins/specweave-github/lib/github-graphql-client.js +138 -0
  29. package/dist/plugins/specweave-github/lib/github-graphql-client.js.map +1 -0
  30. package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts +40 -0
  31. package/dist/plugins/specweave-github/lib/github-issue-body-generator.d.ts.map +1 -0
  32. package/dist/plugins/specweave-github/lib/github-issue-body-generator.js +50 -0
  33. package/dist/plugins/specweave-github/lib/github-issue-body-generator.js.map +1 -0
  34. package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts +30 -0
  35. package/dist/plugins/specweave-github/lib/github-issue-body-parser.d.ts.map +1 -0
  36. package/dist/plugins/specweave-github/lib/github-issue-body-parser.js +75 -0
  37. package/dist/plugins/specweave-github/lib/github-issue-body-parser.js.map +1 -0
  38. package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts +94 -0
  39. package/dist/plugins/specweave-github/lib/github-pull-sync.d.ts.map +1 -0
  40. package/dist/plugins/specweave-github/lib/github-pull-sync.js +232 -0
  41. package/dist/plugins/specweave-github/lib/github-pull-sync.js.map +1 -0
  42. package/dist/plugins/specweave-github/lib/github-push-sync.d.ts +50 -0
  43. package/dist/plugins/specweave-github/lib/github-push-sync.d.ts.map +1 -0
  44. package/dist/plugins/specweave-github/lib/github-push-sync.js +114 -0
  45. package/dist/plugins/specweave-github/lib/github-push-sync.js.map +1 -0
  46. package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts +53 -0
  47. package/dist/plugins/specweave-github/lib/github-rate-limiter.d.ts.map +1 -0
  48. package/dist/plugins/specweave-github/lib/github-rate-limiter.js +109 -0
  49. package/dist/plugins/specweave-github/lib/github-rate-limiter.js.map +1 -0
  50. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts +21 -0
  51. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.d.ts.map +1 -0
  52. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +161 -0
  53. package/dist/plugins/specweave-github/lib/github-spec-frontmatter-updater.js.map +1 -0
  54. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts +46 -0
  55. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.d.ts.map +1 -0
  56. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js +99 -0
  57. package/dist/plugins/specweave-github/lib/github-sync-orchestrator.js.map +1 -0
  58. package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts +43 -0
  59. package/dist/plugins/specweave-github/lib/github-us-auto-closer.d.ts.map +1 -0
  60. package/dist/plugins/specweave-github/lib/github-us-auto-closer.js +153 -0
  61. package/dist/plugins/specweave-github/lib/github-us-auto-closer.js.map +1 -0
  62. package/dist/plugins/specweave-github/lib/index.d.ts +1 -4
  63. package/dist/plugins/specweave-github/lib/index.d.ts.map +1 -1
  64. package/dist/plugins/specweave-github/lib/index.js +1 -4
  65. package/dist/plugins/specweave-github/lib/index.js.map +1 -1
  66. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +7 -0
  67. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts.map +1 -0
  68. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js +15 -0
  69. package/dist/plugins/specweave-testing/lib/playwright-ci-defaults.js.map +1 -0
  70. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts +10 -0
  71. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.d.ts.map +1 -0
  72. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js +36 -0
  73. package/dist/plugins/specweave-testing/lib/playwright-cli-detector.js.map +1 -0
  74. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts +25 -0
  75. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.d.ts.map +1 -0
  76. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js +57 -0
  77. package/dist/plugins/specweave-testing/lib/playwright-cli-runner.js.map +1 -0
  78. package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts +7 -0
  79. package/dist/plugins/specweave-testing/lib/playwright-routing.d.ts.map +1 -0
  80. package/dist/plugins/specweave-testing/lib/playwright-routing.js +17 -0
  81. package/dist/plugins/specweave-testing/lib/playwright-routing.js.map +1 -0
  82. package/dist/src/cli/commands/auto.d.ts.map +1 -1
  83. package/dist/src/cli/commands/auto.js +1 -2
  84. package/dist/src/cli/commands/auto.js.map +1 -1
  85. package/dist/src/cli/commands/cancel-auto.js +1 -2
  86. package/dist/src/cli/commands/cancel-auto.js.map +1 -1
  87. package/dist/src/cli/commands/living-docs.js +2 -2
  88. package/dist/src/cli/commands/living-docs.js.map +1 -1
  89. package/dist/src/cli/commands/update.d.ts.map +1 -1
  90. package/dist/src/cli/commands/update.js +1 -2
  91. package/dist/src/cli/commands/update.js.map +1 -1
  92. package/dist/src/core/config/types.d.ts +8 -0
  93. package/dist/src/core/config/types.d.ts.map +1 -1
  94. package/dist/src/core/config/types.js +3 -0
  95. package/dist/src/core/config/types.js.map +1 -1
  96. package/dist/src/core/types/sync-profile.d.ts +72 -0
  97. package/dist/src/core/types/sync-profile.d.ts.map +1 -1
  98. package/dist/src/core/types/sync-profile.js +6 -0
  99. package/dist/src/core/types/sync-profile.js.map +1 -1
  100. package/package.json +2 -2
  101. package/plugins/specweave/hooks/hooks.json +2 -2
  102. package/plugins/specweave/hooks/startup-health-check.sh +1 -1
  103. package/plugins/specweave/hooks/stop-auto-v5.sh +166 -0
  104. package/plugins/specweave/hooks/user-prompt-submit.sh +10 -0
  105. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +21 -1
  106. package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -1
  107. package/plugins/specweave/skills/auto/SKILL.md +71 -251
  108. package/plugins/specweave/skills/team-build/SKILL.md +370 -0
  109. package/plugins/specweave/skills/team-merge/SKILL.md +123 -0
  110. package/plugins/specweave/skills/team-orchestrate/SKILL.md +800 -0
  111. package/plugins/specweave/skills/team-status/SKILL.md +89 -0
  112. package/plugins/specweave-github/MULTI-PROJECT-SYNC-ARCHITECTURE.md +94 -8
  113. package/plugins/specweave-github/commands/sync.md +17 -3
  114. package/plugins/specweave-github/hooks/github-ac-sync-handler.sh +255 -0
  115. package/plugins/specweave-github/hooks/github-auto-create-handler.sh +455 -0
  116. package/plugins/specweave-github/lib/github-ac-comment-poster.js +150 -0
  117. package/plugins/specweave-github/lib/github-ac-comment-poster.ts +245 -0
  118. package/plugins/specweave-github/lib/github-batch-sync.js +93 -0
  119. package/plugins/specweave-github/lib/github-batch-sync.ts +152 -0
  120. package/plugins/specweave-github/lib/github-board-resolver-v2.js +47 -0
  121. package/plugins/specweave-github/lib/github-board-resolver-v2.ts +73 -0
  122. package/plugins/specweave-github/lib/github-conflict-resolver.js +90 -0
  123. package/plugins/specweave-github/lib/github-conflict-resolver.ts +154 -0
  124. package/plugins/specweave-github/lib/github-cross-repo-sync.js +168 -0
  125. package/plugins/specweave-github/lib/github-cross-repo-sync.ts +252 -0
  126. package/plugins/specweave-github/lib/github-field-sync.js +116 -0
  127. package/plugins/specweave-github/lib/github-field-sync.ts +165 -0
  128. package/plugins/specweave-github/lib/github-graphql-client.js +129 -0
  129. package/plugins/specweave-github/lib/github-graphql-client.ts +181 -0
  130. package/plugins/specweave-github/lib/github-issue-body-generator.js +30 -0
  131. package/plugins/specweave-github/lib/github-issue-body-generator.ts +76 -0
  132. package/plugins/specweave-github/lib/github-issue-body-parser.js +55 -0
  133. package/plugins/specweave-github/lib/github-issue-body-parser.ts +92 -0
  134. package/plugins/specweave-github/lib/github-pull-sync.js +185 -0
  135. package/plugins/specweave-github/lib/github-pull-sync.ts +343 -0
  136. package/plugins/specweave-github/lib/github-push-sync.js +119 -0
  137. package/plugins/specweave-github/lib/github-push-sync.ts +174 -0
  138. package/plugins/specweave-github/lib/github-rate-limiter.js +96 -0
  139. package/plugins/specweave-github/lib/github-rate-limiter.ts +143 -0
  140. package/plugins/specweave-github/lib/github-spec-frontmatter-updater.js +117 -0
  141. package/plugins/specweave-github/lib/github-spec-frontmatter-updater.ts +180 -0
  142. package/plugins/specweave-github/lib/github-sync-orchestrator.js +84 -0
  143. package/plugins/specweave-github/lib/github-sync-orchestrator.ts +156 -0
  144. package/plugins/specweave-github/lib/github-us-auto-closer.js +134 -0
  145. package/plugins/specweave-github/lib/github-us-auto-closer.ts +226 -0
  146. package/plugins/specweave-github/lib/index.js +1 -7
  147. package/plugins/specweave-github/lib/index.ts +1 -4
  148. package/plugins/specweave-github/skills/github-sync/SKILL.md +76 -4
  149. package/plugins/specweave-testing/commands/e2e-setup.md +18 -0
  150. package/plugins/specweave-testing/commands/ui-automate.md +2 -0
  151. package/plugins/specweave-testing/commands/ui-inspect.md +8 -0
  152. package/plugins/specweave-testing/lib/playwright-ci-defaults.d.ts +6 -0
  153. package/plugins/specweave-testing/lib/playwright-ci-defaults.js +14 -0
  154. package/plugins/specweave-testing/lib/playwright-ci-defaults.ts +24 -0
  155. package/plugins/specweave-testing/lib/playwright-cli-detector.js +33 -0
  156. package/plugins/specweave-testing/lib/playwright-cli-detector.ts +48 -0
  157. package/plugins/specweave-testing/lib/playwright-cli-runner.js +58 -0
  158. package/plugins/specweave-testing/lib/playwright-cli-runner.ts +80 -0
  159. package/plugins/specweave-testing/lib/playwright-routing.js +16 -0
  160. package/plugins/specweave-testing/lib/playwright-routing.ts +38 -0
  161. package/plugins/specweave-testing/skills/e2e-testing/SKILL.md +38 -0
  162. package/src/templates/CLAUDE.md.template +7 -0
  163. package/src/templates/config.json.template +9 -1
  164. package/dist/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
  165. package/dist/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
  166. package/dist/plugins/specweave-github/lib/subtask-sync.js +0 -147
  167. package/dist/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
  168. package/dist/plugins/specweave-github/lib/task-parser.d.ts +0 -37
  169. package/dist/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
  170. package/dist/plugins/specweave-github/lib/task-parser.js +0 -211
  171. package/dist/plugins/specweave-github/lib/task-parser.js.map +0 -1
  172. package/dist/plugins/specweave-github/lib/task-sync.d.ts +0 -56
  173. package/dist/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
  174. package/dist/plugins/specweave-github/lib/task-sync.js +0 -375
  175. package/dist/plugins/specweave-github/lib/task-sync.js.map +0 -1
  176. package/plugins/specweave/hooks/validate-completion-conditions.sh +0 -474
  177. package/plugins/specweave-github/lib/subtask-sync.d.ts +0 -51
  178. package/plugins/specweave-github/lib/subtask-sync.d.ts.map +0 -1
  179. package/plugins/specweave-github/lib/subtask-sync.js +0 -154
  180. package/plugins/specweave-github/lib/subtask-sync.js.map +0 -1
  181. package/plugins/specweave-github/lib/subtask-sync.ts +0 -225
  182. package/plugins/specweave-github/lib/task-parser.d.js +0 -0
  183. package/plugins/specweave-github/lib/task-parser.d.ts +0 -37
  184. package/plugins/specweave-github/lib/task-parser.d.ts.map +0 -1
  185. package/plugins/specweave-github/lib/task-parser.js +0 -195
  186. package/plugins/specweave-github/lib/task-parser.js.map +0 -1
  187. package/plugins/specweave-github/lib/task-parser.ts +0 -246
  188. package/plugins/specweave-github/lib/task-sync.d.js +0 -0
  189. package/plugins/specweave-github/lib/task-sync.d.ts +0 -51
  190. package/plugins/specweave-github/lib/task-sync.d.ts.map +0 -1
  191. package/plugins/specweave-github/lib/task-sync.js +0 -415
  192. package/plugins/specweave-github/lib/task-sync.js.map +0 -1
  193. package/plugins/specweave-github/lib/task-sync.ts +0 -451
  194. package/plugins/specweave-github/skills/github-issue-tracker/SKILL.md +0 -496
  195. /package/plugins/specweave/hooks/{stop-auto.sh → _archive/stop-auto-v4-legacy.sh} +0 -0
  196. /package/plugins/{specweave-github/lib/subtask-sync.d.js → specweave-testing/lib/playwright-ci-defaults.d.js} +0 -0
@@ -0,0 +1,455 @@
1
+ #!/bin/bash
2
+ # github-auto-create-handler.sh — Auto-creates GitHub issues from spec.md user stories
3
+ #
4
+ # Triggered by post-tool-use.sh when spec.md is created/updated.
5
+ # Pure bash using gh CLI — no TypeScript compilation required.
6
+ #
7
+ # Features:
8
+ # - Idempotent: checks existing issues before creating
9
+ # - Updates metadata.json with external links
10
+ # - Debounce (10s window to batch rapid spec edits)
11
+ # - Circuit breaker (3 consecutive failures → auto-disable)
12
+ # - Non-blocking (all errors → exit 0)
13
+ #
14
+ # Usage: github-auto-create-handler.sh <INCREMENT_ID>
15
+ # Called by: safe_run_background in post-tool-use.sh
16
+
17
+ set +e # Never crash Claude Code
18
+
19
+ [[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
20
+
21
+ # ============================================================================
22
+ # ARGS & PROJECT ROOT
23
+ # ============================================================================
24
+
25
+ INC_ID="$1"
26
+ [[ -z "$INC_ID" ]] && exit 0
27
+
28
+ PROJECT_ROOT="$PWD"
29
+ while [[ "$PROJECT_ROOT" != "/" ]] && [[ ! -d "$PROJECT_ROOT/.specweave" ]]; do
30
+ PROJECT_ROOT=$(dirname "$PROJECT_ROOT")
31
+ done
32
+ [[ ! -d "$PROJECT_ROOT/.specweave" ]] && exit 0
33
+
34
+ SPEC_PATH="$PROJECT_ROOT/.specweave/increments/$INC_ID/spec.md"
35
+ META_PATH="$PROJECT_ROOT/.specweave/increments/$INC_ID/metadata.json"
36
+ CONFIG_PATH="$PROJECT_ROOT/.specweave/config.json"
37
+ STATE_DIR="$PROJECT_ROOT/.specweave/state"
38
+ LOGS_DIR="$PROJECT_ROOT/.specweave/logs"
39
+ LOG_FILE="$LOGS_DIR/github-auto-create.log"
40
+
41
+ mkdir -p "$STATE_DIR" "$LOGS_DIR" 2>/dev/null || true
42
+
43
+ log() {
44
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] [github-auto-create] $1" >> "$LOG_FILE" 2>/dev/null || true
45
+ }
46
+
47
+ # ============================================================================
48
+ # PRECONDITIONS
49
+ # ============================================================================
50
+
51
+ [[ ! -f "$SPEC_PATH" ]] && exit 0
52
+ [[ ! -f "$CONFIG_PATH" ]] && exit 0
53
+
54
+ command -v gh &>/dev/null || { log "gh CLI not found"; exit 0; }
55
+ command -v jq &>/dev/null || { log "jq not found"; exit 0; }
56
+
57
+ # Check GitHub sync enabled
58
+ GH_ENABLED=$(jq -r '.sync.github.enabled // false' "$CONFIG_PATH" 2>/dev/null)
59
+ [[ "$GH_ENABLED" != "true" ]] && { log "GitHub sync not enabled"; exit 0; }
60
+
61
+ # Check auto-create enabled (either autoSync OR auto_create_github_issue)
62
+ AUTO_SYNC=$(jq -r '.sync.autoSync // false' "$CONFIG_PATH" 2>/dev/null)
63
+ AUTO_CREATE=$(jq -r '.hooks.post_increment_planning.auto_create_github_issue // false' "$CONFIG_PATH" 2>/dev/null)
64
+ if [[ "$AUTO_SYNC" != "true" ]] && [[ "$AUTO_CREATE" != "true" ]]; then
65
+ log "Auto-sync disabled (autoSync=$AUTO_SYNC, auto_create=$AUTO_CREATE)"
66
+ exit 0
67
+ fi
68
+
69
+ # Get owner/repo
70
+ OWNER=$(jq -r '.sync.github.owner // ""' "$CONFIG_PATH" 2>/dev/null)
71
+ REPO=$(jq -r '.sync.github.repo // ""' "$CONFIG_PATH" 2>/dev/null)
72
+ if [[ -z "$OWNER" ]] || [[ -z "$REPO" ]]; then
73
+ log "GitHub owner/repo not configured"
74
+ exit 0
75
+ fi
76
+ REPO_SLUG="$OWNER/$REPO"
77
+
78
+ # Get project name from config for labels
79
+ PROJECT_NAME=$(jq -r '.project.name // "specweave"' "$CONFIG_PATH" 2>/dev/null)
80
+ PROJECT_LABEL="project:${PROJECT_NAME}"
81
+
82
+ # ============================================================================
83
+ # CIRCUIT BREAKER
84
+ # ============================================================================
85
+
86
+ CB_FILE="$STATE_DIR/.hook-circuit-breaker-github-auto-create"
87
+ if [[ -f "$CB_FILE" ]]; then
88
+ CB_COUNT=$(cat "$CB_FILE" 2>/dev/null || echo 0)
89
+ if (( CB_COUNT >= 3 )); then
90
+ log "Circuit breaker OPEN ($CB_COUNT failures). Skipping. Delete $CB_FILE to reset."
91
+ exit 0
92
+ fi
93
+ fi
94
+
95
+ # ============================================================================
96
+ # DEBOUNCE (10s window — spec.md may be written multiple times during planning)
97
+ # ============================================================================
98
+
99
+ if [[ "${SPECWEAVE_SKIP_DEBOUNCE:-0}" != "1" ]]; then
100
+ DEBOUNCE_FILE="$STATE_DIR/.github-auto-create-pending-$INC_ID"
101
+ if [[ -f "$DEBOUNCE_FILE" ]]; then
102
+ SIGNAL_AGE=$(($(date +%s) - $(stat -f "%m" "$DEBOUNCE_FILE" 2>/dev/null || echo 0)))
103
+ if (( SIGNAL_AGE < 10 )); then
104
+ log "Debounce: signal age ${SIGNAL_AGE}s < 10s. Deferring."
105
+ exit 0
106
+ fi
107
+ fi
108
+ echo "$(date +%s)" > "$DEBOUNCE_FILE" 2>/dev/null || true
109
+ sleep 10
110
+ # Check if a newer invocation took over
111
+ if [[ -f "$DEBOUNCE_FILE" ]]; then
112
+ CURRENT_SIGNAL=$(cat "$DEBOUNCE_FILE" 2>/dev/null || echo 0)
113
+ NOW=$(date +%s)
114
+ SIGNAL_AGE=$((NOW - CURRENT_SIGNAL))
115
+ if (( SIGNAL_AGE > 12 )); then
116
+ log "Debounce: newer invocation detected. Exiting."
117
+ exit 0
118
+ fi
119
+ fi
120
+ rm -f "$DEBOUNCE_FILE" 2>/dev/null || true
121
+ fi
122
+
123
+ # ============================================================================
124
+ # CHECK IF ISSUES ALREADY EXIST
125
+ # ============================================================================
126
+
127
+ if [[ -f "$META_PATH" ]]; then
128
+ # Check if there are actual issues with issueNumber values (not just empty {})
129
+ ISSUE_COUNT=$(jq '[.externalLinks.github.issues // {} | to_entries[] | select(.value.issueNumber != null)] | length' "$META_PATH" 2>/dev/null || echo 0)
130
+ if (( ISSUE_COUNT > 0 )); then
131
+ log "Issues already exist for $INC_ID ($ISSUE_COUNT). Skipping."
132
+ exit 0
133
+ fi
134
+ fi
135
+
136
+ # ============================================================================
137
+ # PARSE USER STORIES FROM SPEC.MD
138
+ # ============================================================================
139
+
140
+ # Get default priority from metadata.json
141
+ DEFAULT_PRIORITY="P2"
142
+ if [[ -f "$META_PATH" ]]; then
143
+ META_PRIORITY=$(jq -r '.priority // "P2"' "$META_PATH" 2>/dev/null)
144
+ [[ -n "$META_PRIORITY" ]] && DEFAULT_PRIORITY="$META_PRIORITY"
145
+ fi
146
+
147
+ log "Parsing user stories from $SPEC_PATH..."
148
+
149
+ # Arrays to hold parsed user stories
150
+ declare -a US_IDS US_TITLES US_PRIORITIES
151
+ US_COUNT=0
152
+
153
+ while IFS= read -r line; do
154
+ # Match: ### US-001: Title Here (P1) — with priority
155
+ if [[ "$line" =~ ^###[[:space:]]+(US-[0-9]+):[[:space:]]+(.+)[[:space:]]+\((P[0-9])\)[[:space:]]*$ ]]; then
156
+ US_IDS+=("${BASH_REMATCH[1]}")
157
+ US_TITLES+=("${BASH_REMATCH[2]}")
158
+ US_PRIORITIES+=("${BASH_REMATCH[3]}")
159
+ ((US_COUNT++))
160
+ # Match: ### US-001: Title Here — without priority
161
+ elif [[ "$line" =~ ^###[[:space:]]+(US-[0-9]+):[[:space:]]+(.+)[[:space:]]*$ ]]; then
162
+ US_IDS+=("${BASH_REMATCH[1]}")
163
+ US_TITLES+=("${BASH_REMATCH[2]}")
164
+ # Try to extract priority from metadata.json or default to P2
165
+ US_PRIORITIES+=("${DEFAULT_PRIORITY:-P2}")
166
+ ((US_COUNT++))
167
+ fi
168
+ done < "$SPEC_PATH"
169
+
170
+ if (( US_COUNT == 0 )); then
171
+ log "No user stories found in spec.md. Skipping."
172
+ exit 0
173
+ fi
174
+
175
+ log "Found $US_COUNT user stories. Creating GitHub issues..."
176
+
177
+ # Get increment title from frontmatter
178
+ INC_TITLE=$(grep -m1 '^title:' "$SPEC_PATH" | sed 's/^title:[[:space:]]*//' | sed 's/^"//' | sed 's/"$//')
179
+
180
+ # ============================================================================
181
+ # DERIVE FEATURE ID (ADR-0187: FS-{number} from increment number)
182
+ # ============================================================================
183
+ # Extract numeric prefix from increment ID: "0192-github-sync-v2-multi-repo" → "0192"
184
+ INC_NUM=$(echo "$INC_ID" | grep -oE '^[0-9]+')
185
+ # Strip leading zeros: "0192" → "192", "0001" → "1"
186
+ FEATURE_NUM=$(echo "$INC_NUM" | sed 's/^0*//')
187
+ [[ -z "$FEATURE_NUM" ]] && FEATURE_NUM="0"
188
+ FEATURE_ID="FS-${FEATURE_NUM}"
189
+ log "Derived Feature ID: $FEATURE_ID (from increment $INC_ID)"
190
+
191
+ # ============================================================================
192
+ # CREATE MILESTONE (one per increment, idempotent)
193
+ # ============================================================================
194
+
195
+ # Milestone uses Feature ID format: "FS-192: Short Title"
196
+ MILESTONE_TITLE="${FEATURE_ID}"
197
+ MILESTONE_NUM=""
198
+
199
+ # Check existing milestones (list all, including closed)
200
+ EXISTING_MS=$(gh api "repos/$REPO_SLUG/milestones?state=all&per_page=100" --jq ".[] | select(.title == \"$MILESTONE_TITLE\") | .number" 2>/dev/null || echo "")
201
+ if [[ -n "$EXISTING_MS" ]]; then
202
+ MILESTONE_NUM="$EXISTING_MS"
203
+ log "Using existing milestone #$MILESTONE_NUM"
204
+ else
205
+ MS_RESULT=$(gh api "repos/$REPO_SLUG/milestones" \
206
+ -f "title=$MILESTONE_TITLE" \
207
+ -f "description=$INC_TITLE" \
208
+ --method POST --jq '.number' 2>/dev/null || echo "")
209
+ if [[ -n "$MS_RESULT" ]] && [[ "$MS_RESULT" =~ ^[0-9]+$ ]]; then
210
+ MILESTONE_NUM="$MS_RESULT"
211
+ log "Created milestone #$MILESTONE_NUM: $MILESTONE_TITLE"
212
+ else
213
+ log "Warning: Failed to create milestone (continuing without it)"
214
+ fi
215
+ fi
216
+
217
+ # ============================================================================
218
+ # ENSURE REQUIRED LABELS EXIST (idempotent)
219
+ # ============================================================================
220
+
221
+ ensure_label() {
222
+ local label_name="$1"
223
+ local label_color="${2:-ededed}"
224
+ local label_desc="${3:-}"
225
+ gh label create "$label_name" --repo "$REPO_SLUG" --color "$label_color" \
226
+ --description "$label_desc" --force 2>/dev/null || true
227
+ }
228
+
229
+ # Core labels
230
+ ensure_label "user-story" "0075ca" "User story from SpecWeave spec"
231
+ ensure_label "specweave" "6f42c1" "Managed by SpecWeave"
232
+ ensure_label "$PROJECT_LABEL" "c5def5" "$PROJECT_NAME project"
233
+ ensure_label "status:active" "0e8a16" "Active work item"
234
+
235
+ # Priority labels (collect unique priorities from parsed user stories)
236
+ for i in $(seq 0 $((US_COUNT - 1))); do
237
+ PRIO_LABEL="priority:$(echo "${US_PRIORITIES[$i]}" | tr '[:upper:]' '[:lower:]')"
238
+ case "${US_PRIORITIES[$i]}" in
239
+ P1) ensure_label "$PRIO_LABEL" "b60205" "Critical priority" ;;
240
+ P2) ensure_label "$PRIO_LABEL" "fbca04" "Medium priority" ;;
241
+ P3) ensure_label "$PRIO_LABEL" "0e8a16" "Low priority" ;;
242
+ *) ensure_label "$PRIO_LABEL" "ededed" "Priority label" ;;
243
+ esac
244
+ done
245
+
246
+ log "Labels ensured"
247
+
248
+ # ============================================================================
249
+ # EXTRACT ACCEPTANCE CRITERIA FOR EACH USER STORY
250
+ # ============================================================================
251
+
252
+ # Function to extract ACs for a given US from spec.md
253
+ extract_acs_for_us() {
254
+ local us_id="$1"
255
+ local in_section=false
256
+ local in_ac=false
257
+ local acs=""
258
+
259
+ while IFS= read -r line; do
260
+ # Start of our US section
261
+ if [[ "$line" =~ ^###[[:space:]]+"$us_id": ]]; then
262
+ in_section=true
263
+ continue
264
+ fi
265
+ # Start of next US section — stop
266
+ if $in_section && [[ "$line" =~ ^###[[:space:]]+US- ]]; then
267
+ break
268
+ fi
269
+ # Inside our section, look for AC lines
270
+ if $in_section; then
271
+ if [[ "$line" =~ ^\*\*Acceptance[[:space:]]Criteria ]]; then
272
+ in_ac=true
273
+ continue
274
+ fi
275
+ if $in_ac && [[ "$line" =~ ^-[[:space:]]+\[(.)\][[:space:]]+\*\*([^*]+)\*\*:[[:space:]]*(.*) ]]; then
276
+ local checked="${BASH_REMATCH[1]}"
277
+ local ac_id="${BASH_REMATCH[2]}"
278
+ local ac_desc="${BASH_REMATCH[3]}"
279
+ if [[ "$checked" == "x" ]]; then
280
+ acs+="- [x] **${ac_id}**: ${ac_desc}"$'\n'
281
+ else
282
+ acs+="- [ ] **${ac_id}**: ${ac_desc}"$'\n'
283
+ fi
284
+ fi
285
+ # Stop ACs at next section or empty line after ACs
286
+ if $in_ac && [[ "$line" =~ ^---$ ]]; then
287
+ break
288
+ fi
289
+ fi
290
+ done < "$SPEC_PATH"
291
+
292
+ echo "$acs"
293
+ }
294
+
295
+ # Function to extract description for a given US
296
+ extract_desc_for_us() {
297
+ local us_id="$1"
298
+ local in_section=false
299
+ local desc=""
300
+ local collecting=false
301
+
302
+ while IFS= read -r line; do
303
+ if [[ "$line" =~ ^###[[:space:]]+"$us_id": ]]; then
304
+ in_section=true
305
+ continue
306
+ fi
307
+ if $in_section && [[ "$line" =~ ^###[[:space:]]+US- ]]; then
308
+ break
309
+ fi
310
+ if $in_section; then
311
+ # Collect the As a/I want/So that lines
312
+ if [[ "$line" =~ ^\*\*As\ a\*\* ]] || [[ "$line" =~ ^\*\*I\ want\*\* ]] || [[ "$line" =~ ^\*\*So\ that\*\* ]]; then
313
+ desc+="$line"$'\n'
314
+ fi
315
+ fi
316
+ done < "$SPEC_PATH"
317
+
318
+ echo "$desc"
319
+ }
320
+
321
+ # ============================================================================
322
+ # CREATE ISSUES
323
+ # ============================================================================
324
+
325
+ CREATED_JSON="{}"
326
+ ERRORS=0
327
+
328
+ for i in $(seq 0 $((US_COUNT - 1))); do
329
+ US_ID="${US_IDS[$i]}"
330
+ US_TITLE="${US_TITLES[$i]}"
331
+ US_PRIORITY="${US_PRIORITIES[$i]}"
332
+
333
+ ISSUE_TITLE="[${FEATURE_ID}][${US_ID}] ${US_TITLE}"
334
+
335
+ # Check if issue already exists (by title prefix search — match both old and new format)
336
+ EXISTING=$(gh issue list --repo "$REPO_SLUG" --search "[${FEATURE_ID}][${US_ID}] in:title" --json number,url --limit 1 2>/dev/null || echo "[]")
337
+ EXISTING_NUM=$(echo "$EXISTING" | jq -r '.[0].number // empty' 2>/dev/null)
338
+
339
+ if [[ -n "$EXISTING_NUM" ]]; then
340
+ EXISTING_URL=$(echo "$EXISTING" | jq -r '.[0].url // empty' 2>/dev/null)
341
+ log "Issue already exists for $US_ID: #$EXISTING_NUM"
342
+ CREATED_JSON=$(echo "$CREATED_JSON" | jq --arg us "$US_ID" --arg num "$EXISTING_NUM" --arg url "$EXISTING_URL" \
343
+ '. + {($us): {"issueNumber": ($num | tonumber), "issueUrl": $url, "status": "existing"}}' 2>/dev/null)
344
+ continue
345
+ fi
346
+
347
+ # Build issue body
348
+ US_DESC=$(extract_desc_for_us "$US_ID")
349
+ US_ACS=$(extract_acs_for_us "$US_ID")
350
+
351
+ ISSUE_BODY="## Description
352
+
353
+ ${US_DESC}
354
+ **Priority**: ${US_PRIORITY}
355
+
356
+ ## Acceptance Criteria
357
+
358
+ <!-- specweave:ac-start -->
359
+ ${US_ACS}<!-- specweave:ac-end -->
360
+
361
+ ---
362
+ **Feature**: ${FEATURE_ID} | **Increment**: \`${INC_ID}\`
363
+ <!-- specweave:sync feature=${FEATURE_ID} increment=${INC_ID} us=${US_ID} -->"
364
+
365
+ # Create the issue
366
+ CREATE_ARGS=(
367
+ "issue" "create"
368
+ "--repo" "$REPO_SLUG"
369
+ "--title" "$ISSUE_TITLE"
370
+ "--body" "$ISSUE_BODY"
371
+ "--label" "user-story"
372
+ "--label" "specweave"
373
+ "--label" "$PROJECT_LABEL"
374
+ "--label" "status:active"
375
+ "--label" "priority:$(echo "$US_PRIORITY" | tr '[:upper:]' '[:lower:]')"
376
+ )
377
+
378
+ # Add milestone if available
379
+ if [[ -n "$MILESTONE_NUM" ]]; then
380
+ CREATE_ARGS+=("--milestone" "$MILESTONE_TITLE")
381
+ fi
382
+
383
+ RESULT=$(gh "${CREATE_ARGS[@]}" 2>&1)
384
+ CREATE_EXIT=$?
385
+
386
+ if [[ $CREATE_EXIT -ne 0 ]]; then
387
+ log "ERROR creating issue for $US_ID: $RESULT"
388
+ ((ERRORS++))
389
+
390
+ # Update circuit breaker
391
+ CB_VAL=$(cat "$CB_FILE" 2>/dev/null || echo 0)
392
+ echo "$((CB_VAL + 1))" > "$CB_FILE" 2>/dev/null || true
393
+ continue
394
+ fi
395
+
396
+ # Parse issue URL from result (gh outputs the URL on success)
397
+ ISSUE_URL=$(echo "$RESULT" | grep -oE 'https://github.com/[^ ]+' | head -1)
398
+ ISSUE_NUM=$(echo "$ISSUE_URL" | grep -oE '[0-9]+$')
399
+
400
+ if [[ -n "$ISSUE_NUM" ]]; then
401
+ log "Created issue #$ISSUE_NUM for $US_ID: $ISSUE_URL"
402
+ CREATED_JSON=$(echo "$CREATED_JSON" | jq --arg us "$US_ID" --arg num "$ISSUE_NUM" --arg url "$ISSUE_URL" \
403
+ '. + {($us): {"issueNumber": ($num | tonumber), "issueUrl": $url, "status": "created"}}' 2>/dev/null)
404
+ fi
405
+ done
406
+
407
+ # ============================================================================
408
+ # UPDATE METADATA.JSON WITH EXTERNAL LINKS
409
+ # ============================================================================
410
+
411
+ if [[ -f "$META_PATH" ]]; then
412
+ UPDATED_META=$(jq --argjson issues "$CREATED_JSON" --arg ms "${MILESTONE_NUM:-}" \
413
+ '.externalLinks.github = {
414
+ "issues": $issues,
415
+ "milestone": (if $ms != "" then ($ms | tonumber) else null end),
416
+ "syncedAt": (now | strftime("%Y-%m-%dT%H:%M:%SZ"))
417
+ }' "$META_PATH" 2>/dev/null)
418
+
419
+ if [[ -n "$UPDATED_META" ]]; then
420
+ echo "$UPDATED_META" > "$META_PATH" 2>/dev/null
421
+ log "Updated metadata.json with GitHub issue links"
422
+ fi
423
+ fi
424
+
425
+ # ============================================================================
426
+ # UPDATE SYNC METADATA
427
+ # ============================================================================
428
+
429
+ SYNC_META="$PROJECT_ROOT/.specweave/sync-metadata.json"
430
+ if [[ -f "$SYNC_META" ]]; then
431
+ jq --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
432
+ '.github.lastSync = $ts | .github.lastSyncResult = "success"' \
433
+ "$SYNC_META" > "${SYNC_META}.tmp" 2>/dev/null && \
434
+ mv "${SYNC_META}.tmp" "$SYNC_META" 2>/dev/null
435
+ fi
436
+
437
+ # Reset circuit breaker on success
438
+ if (( ERRORS == 0 )); then
439
+ echo "0" > "$CB_FILE" 2>/dev/null || true
440
+ fi
441
+
442
+ CREATED_COUNT=$(echo "$CREATED_JSON" | jq 'to_entries | map(select(.value.status == "created")) | length' 2>/dev/null || echo 0)
443
+ EXISTING_COUNT=$(echo "$CREATED_JSON" | jq 'to_entries | map(select(.value.status == "existing")) | length' 2>/dev/null || echo 0)
444
+
445
+ log "Done: $CREATED_COUNT created, $EXISTING_COUNT existing, $ERRORS errors"
446
+
447
+ # Output summary for user visibility (will appear as hook output)
448
+ if (( CREATED_COUNT > 0 )); then
449
+ echo ""
450
+ echo " [GitHub Sync] Auto-created $CREATED_COUNT issue(s) for increment $INC_ID"
451
+ echo " Milestone: $MILESTONE_TITLE"
452
+ echo ""
453
+ fi
454
+
455
+ exit 0
@@ -0,0 +1,150 @@
1
+ import { readFile } from "fs/promises";
2
+ import { execFileNoThrow } from "../../../src/utils/execFileNoThrow.js";
3
+ import { pushSyncUserStories } from "./github-push-sync.js";
4
+ async function postACProgressComments(incrementId, affectedUSIds, specPath, options) {
5
+ const result = { posted: [], errors: [] };
6
+ if (affectedUSIds.length === 0) {
7
+ return result;
8
+ }
9
+ let content;
10
+ try {
11
+ content = await readFile(specPath, "utf-8");
12
+ } catch (err) {
13
+ result.errors.push({
14
+ usId: affectedUSIds[0],
15
+ error: err instanceof Error ? err.message : String(err)
16
+ });
17
+ return result;
18
+ }
19
+ const issueLinks = parseIssueLinks(content);
20
+ const repoSlug = `${options.owner}/${options.repo}`;
21
+ const env = options.token ? { GH_TOKEN: options.token } : void 0;
22
+ for (const usId of affectedUSIds) {
23
+ const link = issueLinks[usId];
24
+ if (!link) {
25
+ continue;
26
+ }
27
+ const acStates = parseACStatesForUS(content, usId);
28
+ const commentBody = buildProgressCommentForUS(incrementId, usId, acStates);
29
+ const execResult = await execFileNoThrow(
30
+ "gh",
31
+ ["issue", "comment", String(link.issueNumber), "--body", commentBody, "-R", repoSlug],
32
+ env ? { env } : {}
33
+ );
34
+ if (execResult.success) {
35
+ result.posted.push({ usId, issueNumber: link.issueNumber });
36
+ } else {
37
+ result.errors.push({
38
+ usId,
39
+ error: execResult.stderr || "Unknown error posting comment"
40
+ });
41
+ }
42
+ const usForSync = buildUserStoryForSync(content, usId, acStates, incrementId);
43
+ if (usForSync) {
44
+ try {
45
+ await pushSyncUserStories([usForSync], {
46
+ owner: options.owner,
47
+ repo: options.repo,
48
+ token: options.token
49
+ });
50
+ } catch {
51
+ }
52
+ }
53
+ }
54
+ return result;
55
+ }
56
+ function parseIssueLinks(content) {
57
+ const links = {};
58
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
59
+ if (!fmMatch) return links;
60
+ const frontmatter = fmMatch[1];
61
+ const usBlockMatch = frontmatter.match(/userStories:\s*\n((?:\s{6,}[\s\S]*?)(?=\n\s{0,3}\S|$))/);
62
+ if (!usBlockMatch) return links;
63
+ const usBlock = usBlockMatch[1];
64
+ const usEntries = usBlock.match(/^\s+(US-\d+):\s*\n((?:\s+\w[\s\S]*?)(?=\n\s+US-|\s*$))/gm);
65
+ if (!usEntries) return links;
66
+ for (const entry of usEntries) {
67
+ const idMatch = entry.match(/(US-\d+):/);
68
+ const numMatch = entry.match(/issueNumber:\s*(\d+)/);
69
+ const urlMatch = entry.match(/issueUrl:\s*"([^"]+)"/);
70
+ if (idMatch && numMatch) {
71
+ links[idMatch[1]] = {
72
+ issueNumber: parseInt(numMatch[1], 10),
73
+ issueUrl: urlMatch ? urlMatch[1] : ""
74
+ };
75
+ }
76
+ }
77
+ return links;
78
+ }
79
+ function parseACStatesForUS(content, usId) {
80
+ const states = [];
81
+ const usNum = String(parseInt(usId.replace("US-", ""), 10));
82
+ const acPattern = new RegExp(
83
+ `- \\[([ x])\\] \\*\\*AC-US${usNum}-(\\d+)\\*\\*:\\s*(.+)`,
84
+ "g"
85
+ );
86
+ let match;
87
+ while ((match = acPattern.exec(content)) !== null) {
88
+ states.push({
89
+ id: `AC-US${usNum}-${match[2]}`,
90
+ description: match[3].trim(),
91
+ completed: match[1] === "x"
92
+ });
93
+ }
94
+ return states;
95
+ }
96
+ function buildUserStoryForSync(content, usId, acStates, incrementId) {
97
+ const usNum = String(parseInt(usId.replace("US-", ""), 10)).padStart(3, "0");
98
+ const titleMatch = content.match(new RegExp(`### US-${usNum}:\\s*(.+)`));
99
+ const title = titleMatch ? titleMatch[1].trim() : usId;
100
+ return {
101
+ id: usId,
102
+ title,
103
+ description: "",
104
+ priority: "P1",
105
+ status: "in-progress",
106
+ acceptanceCriteria: acStates.map((ac) => ({
107
+ id: ac.id,
108
+ description: ac.description,
109
+ completed: ac.completed
110
+ })),
111
+ specId: incrementId
112
+ };
113
+ }
114
+ function buildProgressCommentForUS(incrementId, usId, acStates) {
115
+ const total = acStates.length;
116
+ const completed = acStates.filter((ac) => ac.completed).length;
117
+ const percentage = total > 0 ? Math.round(completed / total * 100) : 0;
118
+ let comment = `**Progress Update** \u2014 ${usId} (Increment ${incrementId})
119
+
120
+ `;
121
+ comment += `**Status**: ${completed}/${total} ACs complete (${percentage}%)
122
+
123
+ `;
124
+ if (completed > 0) {
125
+ comment += `**Completed**:
126
+ `;
127
+ for (const ac of acStates.filter((a) => a.completed)) {
128
+ comment += `- [x] **${ac.id}**: ${ac.description}
129
+ `;
130
+ }
131
+ comment += "\n";
132
+ }
133
+ if (completed < total) {
134
+ comment += `**Remaining**:
135
+ `;
136
+ for (const ac of acStates.filter((a) => !a.completed)) {
137
+ comment += `- [ ] **${ac.id}**: ${ac.description}
138
+ `;
139
+ }
140
+ comment += "\n";
141
+ }
142
+ comment += `---
143
+ `;
144
+ comment += `Auto-synced by SpecWeave | ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
145
+ `;
146
+ return comment;
147
+ }
148
+ export {
149
+ postACProgressComments
150
+ };