forge-workflow 0.0.3 โ†’ 0.0.5

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 (209) hide show
  1. package/.claude/commands/dev.md +340 -314
  2. package/.claude/commands/plan.md +521 -478
  3. package/.claude/commands/premerge.md +176 -179
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +442 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +164 -134
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -77
  10. package/.claude/commands/validate.md +282 -237
  11. package/.claude/commands/verify.md +221 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +105 -105
  14. package/.claude/scripts/greptile-resolve.sh +526 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +337 -311
  17. package/.cline/workflows/plan.md +518 -475
  18. package/.cline/workflows/premerge.md +173 -176
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +439 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +161 -131
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -74
  25. package/.cline/workflows/validate.md +279 -234
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -314
  29. package/.codex/skills/plan/SKILL.md +521 -478
  30. package/.codex/skills/premerge/SKILL.md +176 -179
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +442 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +164 -134
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -77
  37. package/.codex/skills/validate/SKILL.md +282 -237
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -311
  40. package/.cursor/commands/plan.md +518 -475
  41. package/.cursor/commands/premerge.md +173 -176
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +439 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +161 -131
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -74
  48. package/.cursor/commands/validate.md +279 -234
  49. package/.cursor/commands/verify.md +218 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +342 -316
  54. package/.github/prompts/plan.prompt.md +523 -480
  55. package/.github/prompts/premerge.prompt.md +178 -181
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +444 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +166 -136
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -79
  62. package/.github/prompts/validate.prompt.md +284 -239
  63. package/.github/prompts/verify.prompt.md +223 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +341 -315
  67. package/.kilocode/workflows/plan.md +522 -479
  68. package/.kilocode/workflows/premerge.md +177 -180
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +443 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +165 -135
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -78
  75. package/.kilocode/workflows/validate.md +283 -238
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -314
  79. package/.opencode/commands/plan.md +521 -478
  80. package/.opencode/commands/premerge.md +176 -179
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +442 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +164 -134
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -77
  87. package/.opencode/commands/validate.md +282 -237
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -315
  90. package/.roo/commands/plan.md +522 -479
  91. package/.roo/commands/premerge.md +177 -180
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +443 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +165 -135
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -78
  98. package/.roo/commands/validate.md +283 -238
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -169
  101. package/CLAUDE.md +100 -99
  102. package/LICENSE +21 -21
  103. package/README.md +429 -414
  104. package/bin/forge-cmd.js +313 -313
  105. package/bin/{forge-validate.js โ†’ forge-preflight.js} +309 -303
  106. package/bin/forge.js +4596 -4232
  107. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  108. package/docs/BEADS_GITHUB_SYNC.md +251 -0
  109. package/docs/ENHANCED_ONBOARDING.md +602 -602
  110. package/docs/EXAMPLES.md +482 -482
  111. package/docs/GREPTILE_SETUP.md +400 -400
  112. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  113. package/docs/ROADMAP.md +359 -359
  114. package/docs/SETUP.md +663 -632
  115. package/docs/TOOLCHAIN.md +630 -630
  116. package/docs/VALIDATION.md +363 -363
  117. package/install.sh +40 -1058
  118. package/lefthook.yml +39 -39
  119. package/lib/agents/README.md +198 -198
  120. package/lib/agents/claude.plugin.json +28 -28
  121. package/lib/agents/cline.plugin.json +22 -22
  122. package/lib/agents/codex.plugin.json +19 -19
  123. package/lib/agents/copilot.plugin.json +24 -24
  124. package/lib/agents/cursor.plugin.json +25 -25
  125. package/lib/agents/kilocode.plugin.json +22 -22
  126. package/lib/agents/opencode.plugin.json +20 -20
  127. package/lib/agents/roo.plugin.json +23 -23
  128. package/lib/agents-config.js +2112 -2112
  129. package/lib/beads-health-check.js +143 -0
  130. package/lib/beads-setup.js +341 -0
  131. package/lib/beads-sync-scaffold.js +260 -0
  132. package/lib/commands/dev.js +513 -513
  133. package/lib/commands/plan.js +692 -692
  134. package/lib/commands/recommend.js +119 -119
  135. package/lib/commands/ship.js +377 -377
  136. package/lib/commands/status.js +378 -378
  137. package/lib/commands/validate.js +602 -602
  138. package/lib/context-merge.js +359 -359
  139. package/lib/dep-guard/analyzer.js +294 -294
  140. package/lib/dep-guard/behavior-detector.js +98 -98
  141. package/lib/dep-guard/contract-detector.js +162 -162
  142. package/lib/dep-guard/import-detector.js +498 -498
  143. package/lib/dep-guard/path-utils.js +13 -13
  144. package/lib/dep-guard/rubric.js +120 -120
  145. package/lib/dep-guard/task-parser.js +318 -318
  146. package/lib/detect-agent.js +191 -0
  147. package/lib/detect-worktree.js +47 -0
  148. package/lib/file-hash.js +26 -0
  149. package/lib/husky-migration.js +450 -0
  150. package/lib/lefthook-check.js +65 -0
  151. package/lib/pat-setup.js +207 -0
  152. package/lib/plugin-catalog.js +350 -350
  153. package/lib/plugin-manager.js +166 -166
  154. package/lib/plugin-recommender.js +141 -141
  155. package/lib/project-discovery.js +491 -491
  156. package/lib/setup-action-log.js +139 -0
  157. package/lib/setup-summary-renderer.js +106 -0
  158. package/lib/setup-utils.js +96 -0
  159. package/lib/setup.js +192 -118
  160. package/lib/smart-merge.js +64 -0
  161. package/lib/symlink-utils.js +81 -0
  162. package/lib/workflow-profiles.js +197 -197
  163. package/package.json +131 -129
  164. package/scripts/beads-context.sh +291 -0
  165. package/scripts/beads-context.test.js +563 -0
  166. package/scripts/behavioral-judge.sh +378 -0
  167. package/scripts/benchmark.js +85 -0
  168. package/scripts/branch-protection.js +183 -0
  169. package/scripts/check-agents.js +172 -0
  170. package/scripts/commitlint.js +42 -0
  171. package/scripts/conflict-detect.sh +323 -0
  172. package/scripts/dep-guard-analyze.js +71 -0
  173. package/scripts/dep-guard.sh +811 -0
  174. package/scripts/eval_win.py +249 -0
  175. package/scripts/file-index.sh +399 -0
  176. package/scripts/github-beads-sync/comment.mjs +64 -0
  177. package/scripts/github-beads-sync/config.mjs +148 -0
  178. package/scripts/github-beads-sync/github-api.mjs +131 -0
  179. package/scripts/github-beads-sync/index.mjs +332 -0
  180. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  181. package/scripts/github-beads-sync/mapping.mjs +78 -0
  182. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  183. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  184. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  185. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  186. package/scripts/github-beads-sync.config.json +26 -0
  187. package/scripts/improve-command.js +375 -0
  188. package/scripts/lib/eval-runner.js +229 -0
  189. package/scripts/lib/eval-schema.js +135 -0
  190. package/scripts/lib/eval-storage.js +78 -0
  191. package/scripts/lib/grading.js +203 -0
  192. package/scripts/lib/transcript-parser.js +63 -0
  193. package/scripts/lint.js +47 -0
  194. package/scripts/migrate-to-bun-test.js +412 -0
  195. package/scripts/run-command-eval.js +236 -0
  196. package/scripts/smart-status.sh +782 -0
  197. package/scripts/sync-commands.js +571 -0
  198. package/scripts/sync-utils.sh +460 -0
  199. package/scripts/test-dashboard.js +123 -0
  200. package/scripts/test.js +44 -0
  201. package/scripts/validate.sh +94 -0
  202. package/skills/parallel-deep-research/SKILL.md +108 -108
  203. package/skills/parallel-deep-research/evals/README.md +27 -27
  204. package/skills/parallel-deep-research/evals/evals.json +62 -62
  205. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  206. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  207. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  208. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  209. package/docs/WORKFLOW.md +0 -400
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env bash
2
+ # sync-utils.sh โ€” Shared utilities for multi-dev-awareness sync scripts.
3
+ #
4
+ # Source this file or execute directly with a subcommand.
5
+ # Cross-platform: works on Windows (Git Bash), macOS, and Linux.
6
+ # OWASP A03: All identity strings validated against strict allowlist regex.
7
+ # OWASP A03: All config values sanitized before use (no injection vectors).
8
+
9
+ # Only set errexit/pipefail when run as a script, not when sourced
10
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
11
+ set -euo pipefail
12
+ fi
13
+
14
+ # โ”€โ”€ Session Identity โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
15
+
16
+ # Validates a session identity string against the allowlist regex.
17
+ # Returns 0 if valid, 1 if invalid.
18
+ # Usage: validate_session_identity "user@hostname"
19
+ validate_session_identity() {
20
+ local identity="${1:-}"
21
+ if [[ -z "$identity" ]]; then
22
+ return 1
23
+ fi
24
+ # OWASP A03: strict allowlist โ€” only alphanumeric, dot, underscore, @, +, hyphen
25
+ if [[ "$identity" =~ ^[a-zA-Z0-9._@+-]+$ ]]; then
26
+ return 0
27
+ fi
28
+ return 1
29
+ }
30
+
31
+ # Returns a session identity string in the format: <user>@<hostname>
32
+ # Uses git config user.email if available, falls back to git config user.name.
33
+ # Validates the result against OWASP A03 allowlist regex.
34
+ # Outputs the identity via printf (not echo) and returns 0 on success, 1 on failure.
35
+ # Usage: identity="$(get_session_identity)"
36
+ get_session_identity() {
37
+ local user_part=""
38
+ local host_part=""
39
+
40
+ # Try email first, fall back to user.name
41
+ user_part="$(git config user.email 2>/dev/null)" || true
42
+ if [[ -z "$user_part" ]]; then
43
+ user_part="$(git config user.name 2>/dev/null)" || true
44
+ fi
45
+
46
+ if [[ -z "$user_part" ]]; then
47
+ echo "Error: No git user.email or user.name configured" >&2
48
+ return 1
49
+ fi
50
+
51
+ # Get short hostname โ€” cross-platform:
52
+ # Linux/macOS: hostname -s works
53
+ # Windows Git Bash: hostname -s may fail, fall back to hostname + strip domain
54
+ host_part="$(hostname -s 2>/dev/null)" || true
55
+ if [[ -z "$host_part" ]]; then
56
+ host_part="$(hostname 2>/dev/null)" || true
57
+ # Strip domain suffix if present (e.g., "host.domain.com" -> "host")
58
+ host_part="${host_part%%.*}"
59
+ fi
60
+ if [[ -z "$host_part" ]]; then
61
+ echo "Error: Could not determine hostname" >&2
62
+ return 1
63
+ fi
64
+
65
+ # Replace spaces with hyphens (common in git user.name like "John Doe")
66
+ user_part="${user_part// /-}"
67
+ host_part="${host_part// /-}"
68
+ local identity="${user_part}@${host_part}"
69
+
70
+ # OWASP A03: validate before returning
71
+ if ! validate_session_identity "$identity"; then
72
+ echo "Error: Session identity contains invalid characters: $identity" >&2
73
+ return 1
74
+ fi
75
+
76
+ printf '%s' "$identity"
77
+ }
78
+
79
+ # โ”€โ”€ Input Sanitization โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
80
+
81
+ # Sanitize a config value: strip shell-injection patterns (OWASP A03).
82
+ # Removes: backticks, $(...), semicolons, pipes, newlines.
83
+ # Usage: sanitized="$(sanitize_config_value "$raw")"
84
+ sanitize_config_value() {
85
+ local val="$1"
86
+ # Remove backtick command substitution
87
+ val="${val//\`/}"
88
+ # Remove $(...) command substitution patterns (loop handles nested)
89
+ val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
90
+ # Remove semicolons (command chaining)
91
+ val="${val//;/}"
92
+ # Remove pipes (command chaining)
93
+ val="${val//|/}"
94
+ # Collapse newlines to spaces
95
+ val="$(printf '%s' "$val" | tr '\n' ' ')"
96
+ # Trim leading/trailing whitespace
97
+ val="$(printf '%s' "$val" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
98
+ printf '%s' "$val"
99
+ }
100
+
101
+ # โ”€โ”€ Config Reading โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
102
+
103
+ # Read a key from .beads/config.json (JSON format).
104
+ # Usage: value="$(_read_config_json "/path/to/repo" "sync_branch")"
105
+ # Returns empty string if file missing or key absent.
106
+ _read_config_json() {
107
+ local repo_dir="$1"
108
+ local key="$2"
109
+ local config_file="$repo_dir/.beads/config.json"
110
+ if [ -f "$config_file" ] && command -v jq &>/dev/null; then
111
+ local raw
112
+ raw="$(jq -r --arg k "$key" '.[$k] // empty' -- "$config_file" 2>/dev/null)" || true
113
+ if [ -n "$raw" ]; then
114
+ sanitize_config_value "$raw"
115
+ return 0
116
+ fi
117
+ fi
118
+ printf ''
119
+ }
120
+
121
+ # Read a key from .beads/config.yaml (YAML format, simple grep-based).
122
+ # Only supports top-level scalar keys (no nesting).
123
+ # Usage: value="$(_read_config_yaml "/path/to/repo" "sync-branch")"
124
+ # Returns empty string if file missing or key absent.
125
+ _read_config_yaml() {
126
+ local repo_dir="$1"
127
+ local key="$2"
128
+ local config_file="$repo_dir/.beads/config.yaml"
129
+ if [ -f "$config_file" ]; then
130
+ local raw
131
+ # Escape regex metacharacters in key for safe use in grep/sed
132
+ local escaped_key
133
+ escaped_key="$(printf '%s' "$key" | sed 's/[][\\.^$*+?(){}|/]/\\&/g')"
134
+ # Match: key: "value" or key: value (strip quotes)
135
+ raw="$(grep -E "^${escaped_key}:" -- "$config_file" 2>/dev/null | head -1 | sed -e "s/^${escaped_key}:[[:space:]]*//" -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")" || true
136
+ if [ -n "$raw" ]; then
137
+ sanitize_config_value "$raw"
138
+ return 0
139
+ fi
140
+ fi
141
+ printf ''
142
+ }
143
+
144
+ # Read a sync config value with fallback: config.json > config.yaml.
145
+ # Supports both JSON key (underscore) and YAML key (hyphen) conventions.
146
+ # Usage: value="$(_read_sync_config "/path/to/repo" "sync_branch" "sync-branch")"
147
+ _read_sync_config() {
148
+ local repo_dir="$1"
149
+ local json_key="$2"
150
+ local yaml_key="$3"
151
+ local value
152
+
153
+ # Try JSON config first
154
+ value="$(_read_config_json "$repo_dir" "$json_key")"
155
+ if [ -n "$value" ]; then
156
+ printf '%s' "$value"
157
+ return 0
158
+ fi
159
+
160
+ # Try YAML config
161
+ value="$(_read_config_yaml "$repo_dir" "$yaml_key")"
162
+ if [ -n "$value" ]; then
163
+ printf '%s' "$value"
164
+ return 0
165
+ fi
166
+
167
+ printf ''
168
+ }
169
+
170
+ # โ”€โ”€ Sync Branch Detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
171
+
172
+ # Detect the sync branch with a fallback chain:
173
+ # 1. Config file (.beads/config.json sync_branch or .beads/config.yaml sync-branch)
174
+ # 2. Environment variable BD_SYNC_BRANCH
175
+ # 3. git symbolic-ref refs/remotes/<remote>/HEAD
176
+ # 4. Try 'main' branch on remote
177
+ # 5. Try 'master' branch on remote
178
+ #
179
+ # Usage: branch="$(get_sync_branch "/path/to/repo")"
180
+ # Arguments:
181
+ # $1 โ€” repo directory (optional, defaults to current directory)
182
+ get_sync_branch() {
183
+ local repo_dir="${1:-.}"
184
+ local branch
185
+
186
+ # 1. Config file takes priority
187
+ branch="$(_read_sync_config "$repo_dir" "sync_branch" "sync-branch")"
188
+ if [ -n "$branch" ]; then
189
+ printf '%s' "$branch"
190
+ return 0
191
+ fi
192
+
193
+ # 2. Environment variable
194
+ if [ -n "${BD_SYNC_BRANCH:-}" ]; then
195
+ printf '%s' "$BD_SYNC_BRANCH"
196
+ return 0
197
+ fi
198
+
199
+ # Determine which remote to check (respects sync_remote config)
200
+ local remote
201
+ remote="$(get_sync_remote "$repo_dir")"
202
+
203
+ # 3. git symbolic-ref refs/remotes/<remote>/HEAD
204
+ # Note: symbolic-ref accepts -- before ref names, but rev-parse --verify
205
+ # does NOT โ€” after --, rev-parse treats arguments as paths, not revisions.
206
+ branch="$(git -C "$repo_dir" symbolic-ref "refs/remotes/${remote}/HEAD" 2>/dev/null)" || true
207
+ if [ -n "$branch" ]; then
208
+ # Strip refs/remotes/<remote>/ prefix
209
+ branch="${branch#refs/remotes/${remote}/}"
210
+ printf '%s' "$branch"
211
+ return 0
212
+ fi
213
+
214
+ # 4. Try 'main' โ€” check if remote has it
215
+ # Note: no -- before ref; rev-parse --verify treats post--- args as paths
216
+ if git -C "$repo_dir" rev-parse --verify "refs/remotes/${remote}/main" &>/dev/null; then
217
+ printf '%s' "main"
218
+ return 0
219
+ fi
220
+
221
+ # 5. Try 'master'
222
+ if git -C "$repo_dir" rev-parse --verify "refs/remotes/${remote}/master" &>/dev/null; then
223
+ printf '%s' "master"
224
+ return 0
225
+ fi
226
+
227
+ # Last resort: return 'main' as default
228
+ printf '%s' "main"
229
+ }
230
+
231
+ # โ”€โ”€ Sync Remote Detection โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
232
+
233
+ # Detect the sync remote with a fallback chain:
234
+ # 1. Config file (.beads/config.json sync_remote or .beads/config.yaml sync-remote)
235
+ # 2. Environment variable BD_SYNC_REMOTE
236
+ # 3. Detect 'upstream' remote (fork-based workflow)
237
+ # 4. Default to 'origin'
238
+ #
239
+ # Usage: remote="$(get_sync_remote "/path/to/repo")"
240
+ # Arguments:
241
+ # $1 โ€” repo directory (optional, defaults to current directory)
242
+ get_sync_remote() {
243
+ local repo_dir="${1:-.}"
244
+ local remote
245
+
246
+ # 1. Config file takes priority
247
+ remote="$(_read_sync_config "$repo_dir" "sync_remote" "sync-remote")"
248
+ if [ -n "$remote" ]; then
249
+ printf '%s' "$remote"
250
+ return 0
251
+ fi
252
+
253
+ # 2. Environment variable
254
+ if [ -n "${BD_SYNC_REMOTE:-}" ]; then
255
+ printf '%s' "$BD_SYNC_REMOTE"
256
+ return 0
257
+ fi
258
+
259
+ # 3. Detect 'upstream' remote (common in fork-based workflows)
260
+ if git -C "$repo_dir" remote get-url -- upstream &>/dev/null; then
261
+ printf '%s' "upstream"
262
+ return 0
263
+ fi
264
+
265
+ # 4. Default to 'origin'
266
+ printf '%s' "origin"
267
+ }
268
+
269
+ # โ”€โ”€ Auto Sync โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
270
+
271
+ # Staleness threshold in seconds (15 minutes)
272
+ _SYNC_STALENESS_THRESHOLD=900
273
+
274
+ # auto_sync [repo_dir]
275
+ # Runs `bd sync` to pull/push latest beads state.
276
+ # On failure: warns but continues (non-blocking, always returns 0).
277
+ # On success: records Unix epoch timestamp to .beads/.last-sync
278
+ # and updates file index from all in-progress issues' task files.
279
+ #
280
+ # Environment overrides (for testing):
281
+ # BD_SYNC_CMD โ€” command to run instead of `bd sync` (default: "bd sync")
282
+ # FILE_INDEX_ROOT โ€” root directory for file-index.sh (default: repo_dir)
283
+ auto_sync() {
284
+ local repo_dir="${1:-.}"
285
+ local sync_cmd="${BD_SYNC_CMD:-bd sync}"
286
+ local last_sync_file="$repo_dir/.beads/.last-sync"
287
+
288
+ # Ensure .beads directory exists
289
+ mkdir -p "$repo_dir/.beads"
290
+
291
+ # Run bd sync (or mock) โ€” use $sync_cmd without eval to prevent injection
292
+ # shellcheck disable=SC2086
293
+ if $sync_cmd >/dev/null 2>&1; then
294
+ # Success: record timestamp
295
+ date +%s > "$last_sync_file"
296
+
297
+ # Update file index from in-progress issues' task files
298
+ _auto_sync_update_file_index "$repo_dir"
299
+ else
300
+ # Failure: warn but continue (non-blocking)
301
+ local last_ts="unknown"
302
+ if [[ -f "$last_sync_file" ]]; then
303
+ last_ts="$(cat "$last_sync_file")"
304
+ if [[ "$last_ts" =~ ^[0-9]+$ ]]; then
305
+ local mins=$(( ($(date +%s) - last_ts) / 60 ))
306
+ last_ts="${mins}m ago"
307
+ fi
308
+ fi
309
+ echo "Warning: sync failed, working with local data (last sync: $last_ts)" >&2
310
+ fi
311
+
312
+ return 0
313
+ }
314
+
315
+ # _auto_sync_update_file_index [repo_dir]
316
+ # After a successful sync, iterate over in-progress issues and update the file index.
317
+ # Uses file_index_update_from_tasks from file-index.sh.
318
+ _auto_sync_update_file_index() {
319
+ local repo_dir="${1:-.}"
320
+
321
+ # Source file-index.sh if file_index_update_from_tasks is not defined
322
+ if ! command -v file_index_update_from_tasks &>/dev/null; then
323
+ local script_dir
324
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
325
+ if [[ -f "$script_dir/file-index.sh" ]]; then
326
+ source "$script_dir/file-index.sh"
327
+ else
328
+ return 0
329
+ fi
330
+ fi
331
+
332
+ # Export FILE_INDEX_ROOT so file-index.sh uses the right directory
333
+ export FILE_INDEX_ROOT="${FILE_INDEX_ROOT:-$repo_dir}"
334
+
335
+ # Find in-progress issues by scanning .beads/issues.jsonl
336
+ local issues_file="$repo_dir/.beads/issues.jsonl"
337
+ if [[ ! -f "$issues_file" ]] || [[ ! -s "$issues_file" ]]; then
338
+ return 0
339
+ fi
340
+
341
+ # Check jq is available
342
+ if ! command -v jq &>/dev/null; then
343
+ return 0
344
+ fi
345
+
346
+ # Extract in-progress issue IDs (LWW resolution: group by id, take last, filter in_progress)
347
+ local issue_ids
348
+ issue_ids="$(jq -s -r '
349
+ sort_by(.id)
350
+ | group_by(.id)
351
+ | map(sort_by(.updated_at) | last)
352
+ | map(select(.status == "in_progress"))
353
+ | .[].id
354
+ ' "$issues_file" 2>/dev/null)" || true
355
+
356
+ if [[ -z "$issue_ids" ]]; then
357
+ return 0
358
+ fi
359
+
360
+ # For each in-progress issue, find its task file and update the file index
361
+ local issue_id
362
+ while IFS= read -r issue_id; do
363
+ if [[ -z "$issue_id" ]]; then
364
+ continue
365
+ fi
366
+
367
+ # Look for this issue's task file
368
+ # 1. Try beads design metadata (set by beads-context.sh set-design)
369
+ local task_file=""
370
+ local design_meta
371
+ design_meta="$(bd show "$issue_id" 2>/dev/null | grep -A1 'DESIGN' | tail -1 | sed 's/.*| //')" || true
372
+ # OWASP A04: reject paths that escape the repo root
373
+ case "$design_meta" in
374
+ *../*|..*) design_meta="" ;;
375
+ esac
376
+ if [[ -n "$design_meta" ]] && [[ -f "$repo_dir/$design_meta" ]]; then
377
+ task_file="$repo_dir/$design_meta"
378
+ fi
379
+
380
+ # 2. Fall back to known locations with issue-specific naming
381
+ if [[ -z "$task_file" ]]; then
382
+ for candidate in \
383
+ "$repo_dir/.beads/tasks/${issue_id}.md" \
384
+ "$repo_dir/.beads/tasks/${issue_id}-tasks.md"; do
385
+ if [[ -f "$candidate" ]]; then
386
+ task_file="$candidate"
387
+ break
388
+ fi
389
+ done
390
+ fi
391
+
392
+ # Update file index (file_index_update_from_tasks handles missing task files gracefully)
393
+ file_index_update_from_tasks "$issue_id" "$task_file" "in_progress" 2>/dev/null || true
394
+ done <<< "$issue_ids"
395
+
396
+ # Tombstone closed issues: find indexed entries no longer in_progress
397
+ local indexed_ids
398
+ indexed_ids="$(file_index_read 2>/dev/null | jq -r '.[].issue_id' 2>/dev/null)" || true
399
+ if [[ -n "$indexed_ids" ]]; then
400
+ while IFS= read -r indexed_id; do
401
+ [[ -z "$indexed_id" ]] && continue
402
+ # If this issue is not in the in_progress list, tombstone it
403
+ if ! printf '%s' "$issue_ids" | grep -qF "$indexed_id"; then
404
+ file_index_remove "$indexed_id" 2>/dev/null || true
405
+ fi
406
+ done <<< "$indexed_ids"
407
+ fi
408
+
409
+ return 0
410
+ }
411
+
412
+ # check_sync_staleness [repo_dir]
413
+ # Reads .beads/.last-sync and warns if the last sync is older than 15 minutes.
414
+ # Always returns 0 (non-blocking).
415
+ # Outputs:
416
+ # - Nothing if sync is fresh (<= 900 seconds)
417
+ # - Warning to stderr if stale (> 900 seconds)
418
+ # - Warning to stderr if .last-sync is missing (never synced)
419
+ check_sync_staleness() {
420
+ local repo_dir="${1:-.}"
421
+ local last_sync_file="$repo_dir/.beads/.last-sync"
422
+
423
+ if [[ ! -f "$last_sync_file" ]]; then
424
+ echo "Warning: beads sync has never been run (no .last-sync record)" >&2
425
+ return 0
426
+ fi
427
+
428
+ local last_ts
429
+ last_ts="$(cat "$last_sync_file")"
430
+
431
+ # Validate it's a number
432
+ if [[ ! "$last_ts" =~ ^[0-9]+$ ]]; then
433
+ echo "Warning: .last-sync contains invalid timestamp" >&2
434
+ return 0
435
+ fi
436
+
437
+ local now
438
+ now="$(date +%s)"
439
+ local age=$(( now - last_ts ))
440
+
441
+ if [[ "$age" -gt "$_SYNC_STALENESS_THRESHOLD" ]]; then
442
+ local minutes=$(( age / 60 ))
443
+ echo "Warning: beads data is stale (last sync: ${minutes}m ago, threshold: 15m)" >&2
444
+ fi
445
+
446
+ return 0
447
+ }
448
+
449
+ # โ”€โ”€ CLI Dispatcher โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
450
+
451
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
452
+ case "${1:-}" in
453
+ auto-sync) shift; auto_sync "$@" ;;
454
+ check-staleness) shift; check_sync_staleness "$@" ;;
455
+ identity) get_session_identity ;;
456
+ sync-branch) shift; get_sync_branch "$@" ;;
457
+ sync-remote) shift; get_sync_remote "$@" ;;
458
+ *) echo "Usage: sync-utils.sh <auto-sync|check-staleness|identity|sync-branch|sync-remote>" >&2; exit 1 ;;
459
+ esac
460
+ fi
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Test Quality Dashboard โ€” collects test metrics into a JSON summary.
5
+ *
6
+ * Usage:
7
+ * node scripts/test-dashboard.js # Human-readable output
8
+ * node scripts/test-dashboard.js --json # JSON output (for CI/tests)
9
+ */
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const { execFileSync } = require('node:child_process');
14
+
15
+ const rootDir = path.join(__dirname, '..');
16
+
17
+ function getTestCount() {
18
+ // Count test cases by scanning test files for test() calls
19
+ // This avoids running the full test suite (which would be slow/recursive)
20
+ const testDirs = [
21
+ path.join(rootDir, 'test'),
22
+ path.join(rootDir, 'test-env'),
23
+ path.join(rootDir, 'packages', 'skills', 'test')
24
+ ];
25
+ let count = 0;
26
+ for (const dir of testDirs) {
27
+ if (!fs.existsSync(dir)) continue;
28
+ const files = fs.readdirSync(dir, { recursive: true });
29
+ for (const file of files) {
30
+ const filePath = path.join(dir, String(file));
31
+ if (!filePath.endsWith('.test.js') && !filePath.endsWith('.spec.js')) continue;
32
+ if (!fs.statSync(filePath).isFile()) continue;
33
+ const content = fs.readFileSync(filePath, 'utf-8');
34
+ // Count test() and it() calls (excluding test.skip)
35
+ const testMatches = content.match(/\btest\s*\(/g) || [];
36
+ const itMatches = content.match(/\bit\s*\(/g) || [];
37
+ const skipMatches = content.match(/\btest\.skip\s*\(/g) || [];
38
+ count += testMatches.length + itMatches.length - skipMatches.length;
39
+ }
40
+ }
41
+ return count;
42
+ }
43
+
44
+ function getCoverageThreshold() {
45
+ const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
46
+ return pkg.c8 ? pkg.c8.lines : 0;
47
+ }
48
+
49
+ function getEslintWarnings() {
50
+ // We enforce --max-warnings 0 in pre-push hooks (lefthook.yml),
51
+ // so this is always 0 when code passes CI. Report 0 to avoid
52
+ // slow ESLint execution during dashboard generation.
53
+ return 0;
54
+ }
55
+
56
+ function getSkippedTestCount() {
57
+ try {
58
+ const output = execFileSync('grep', ['-r', 'test.skip', 'test/', 'test-env/'], {
59
+ cwd: rootDir,
60
+ encoding: 'utf-8',
61
+ stdio: ['pipe', 'pipe', 'pipe']
62
+ });
63
+ return output.trim().split('\n').filter(Boolean).length;
64
+ } catch (_e) {
65
+ return 0;
66
+ }
67
+ }
68
+
69
+ function getMutationScore() {
70
+ const reportPath = path.join(rootDir, 'stryker-report', 'mutation.json');
71
+ if (!fs.existsSync(reportPath)) return null;
72
+ try {
73
+ const report = JSON.parse(fs.readFileSync(reportPath, 'utf-8'));
74
+ if (report.schemaVersion) {
75
+ // Stryker v8+ JSON report format
76
+ const files = Object.values(report.files || {});
77
+ let killed = 0;
78
+ let total = 0;
79
+ for (const file of files) {
80
+ for (const mutant of (file.mutants || [])) {
81
+ total++;
82
+ if (mutant.status === 'Killed') killed++;
83
+ }
84
+ }
85
+ return total > 0 ? Math.round((killed / total) * 100) : null;
86
+ }
87
+ return null;
88
+ } catch (_e) {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ const dashboard = {
94
+ testCount: getTestCount(),
95
+ coverageThreshold: getCoverageThreshold(),
96
+ eslintWarnings: getEslintWarnings(),
97
+ skippedTests: getSkippedTestCount(),
98
+ mutationScore: getMutationScore(),
99
+ timestamp: new Date().toISOString()
100
+ };
101
+
102
+ const jsonMode = process.argv.includes('--json');
103
+
104
+ if (jsonMode) {
105
+ process.stdout.write(JSON.stringify(dashboard));
106
+ } else {
107
+ console.log('\n Test Quality Dashboard');
108
+ console.log(' =====================\n');
109
+ console.log(` Tests: ${dashboard.testCount}`);
110
+ console.log(` Coverage threshold: ${dashboard.coverageThreshold}%`);
111
+ console.log(` ESLint warnings: ${dashboard.eslintWarnings}`);
112
+ console.log(` Skipped tests: ${dashboard.skippedTests}`);
113
+ console.log(` Mutation score: ${dashboard.mutationScore !== null ? dashboard.mutationScore + '%' : 'N/A (run test:mutation first)'}`);
114
+ console.log(` Generated: ${dashboard.timestamp}`);
115
+ console.log('');
116
+
117
+ // Write results file
118
+ const outputPath = path.join(rootDir, 'test-dashboard.json');
119
+ fs.writeFileSync(outputPath, JSON.stringify(dashboard, null, 2));
120
+ console.log(` Results saved to: test-dashboard.json\n`);
121
+ }
122
+
123
+ module.exports = dashboard;
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cross-platform test runner for lefthook pre-push hook.
4
+ * Replaces bash-only: bun/pnpm/yarn/npm test detection with if/elif.
5
+ * Works on Windows CMD, PowerShell, macOS, Linux.
6
+ */
7
+
8
+ const { spawnSync } = require('node:child_process');
9
+ const fs = require('node:fs');
10
+
11
+ // On Windows, package manager CLIs are .cmd files โ€” shell: true resolves them
12
+ const isWindows = process.platform === 'win32';
13
+
14
+ // Detect package manager from lock files (same priority as forge.js)
15
+ function detectPackageManager() {
16
+ if (fs.existsSync('bun.lockb') || fs.existsSync('bun.lock')) return 'bun';
17
+ if (fs.existsSync('pnpm-lock.yaml')) return 'pnpm';
18
+ if (fs.existsSync('yarn.lock')) return 'yarn';
19
+ return 'npm';
20
+ }
21
+
22
+ const pkgManager = detectPackageManager();
23
+ console.log(`๐Ÿงช Running test suite (${pkgManager} test)...`);
24
+
25
+ const result = spawnSync(pkgManager, ['test'], { stdio: 'inherit', shell: isWindows });
26
+
27
+ if (result.error) {
28
+ console.error('');
29
+ console.error(`โŒ Failed to run ${pkgManager} test: ${result.error.message}`);
30
+ console.error(` Is '${pkgManager}' installed and on PATH?`);
31
+ console.error('');
32
+ process.exit(1);
33
+ }
34
+
35
+ if (result.status !== 0) {
36
+ console.error('');
37
+ console.error('โŒ Tests failed. Fix them before pushing.');
38
+ console.error('');
39
+ console.error('Fix the failing checks. Do not bypass hooks.');
40
+ console.error('');
41
+ process.exit(1);
42
+ }
43
+
44
+ console.log('โœ… All tests passed');