forge-workflow 0.0.4 โ†’ 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 -340
  2. package/.claude/commands/plan.md +521 -521
  3. package/.claude/commands/premerge.md +176 -176
  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 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -48
  10. package/.claude/commands/validate.md +282 -282
  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 -337
  17. package/.cline/workflows/plan.md +518 -518
  18. package/.cline/workflows/premerge.md +173 -173
  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 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -45
  25. package/.cline/workflows/validate.md +279 -279
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -340
  29. package/.codex/skills/plan/SKILL.md +521 -521
  30. package/.codex/skills/premerge/SKILL.md +176 -176
  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 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -48
  37. package/.codex/skills/validate/SKILL.md +282 -282
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -337
  40. package/.cursor/commands/plan.md +518 -518
  41. package/.cursor/commands/premerge.md +173 -173
  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 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -45
  48. package/.cursor/commands/validate.md +279 -279
  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 -342
  54. package/.github/prompts/plan.prompt.md +523 -523
  55. package/.github/prompts/premerge.prompt.md +178 -178
  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 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -50
  62. package/.github/prompts/validate.prompt.md +284 -284
  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 -341
  67. package/.kilocode/workflows/plan.md +522 -522
  68. package/.kilocode/workflows/premerge.md +177 -177
  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 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -49
  75. package/.kilocode/workflows/validate.md +283 -283
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -340
  79. package/.opencode/commands/plan.md +521 -521
  80. package/.opencode/commands/premerge.md +176 -176
  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 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -48
  87. package/.opencode/commands/validate.md +282 -282
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -341
  90. package/.roo/commands/plan.md +522 -522
  91. package/.roo/commands/premerge.md +177 -177
  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 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -49
  98. package/.roo/commands/validate.md +283 -283
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -175
  101. package/CLAUDE.md +100 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +313 -313
  104. package/bin/forge-preflight.js +309 -309
  105. package/bin/forge.js +4596 -4303
  106. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  107. package/docs/BEADS_GITHUB_SYNC.md +251 -251
  108. package/docs/ENHANCED_ONBOARDING.md +602 -602
  109. package/docs/EXAMPLES.md +482 -482
  110. package/docs/GREPTILE_SETUP.md +400 -400
  111. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  112. package/docs/ROADMAP.md +359 -359
  113. package/docs/SETUP.md +663 -631
  114. package/docs/TOOLCHAIN.md +630 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +39 -39
  118. package/lib/agents/README.md +198 -198
  119. package/lib/agents/claude.plugin.json +28 -28
  120. package/lib/agents/cline.plugin.json +22 -22
  121. package/lib/agents/codex.plugin.json +19 -19
  122. package/lib/agents/copilot.plugin.json +24 -24
  123. package/lib/agents/cursor.plugin.json +25 -25
  124. package/lib/agents/kilocode.plugin.json +22 -22
  125. package/lib/agents/opencode.plugin.json +20 -20
  126. package/lib/agents/roo.plugin.json +23 -23
  127. package/lib/agents-config.js +2112 -2112
  128. package/lib/beads-health-check.js +143 -0
  129. package/lib/beads-setup.js +341 -0
  130. package/lib/beads-sync-scaffold.js +260 -0
  131. package/lib/commands/dev.js +513 -513
  132. package/lib/commands/plan.js +692 -692
  133. package/lib/commands/recommend.js +119 -119
  134. package/lib/commands/ship.js +377 -377
  135. package/lib/commands/status.js +378 -378
  136. package/lib/commands/validate.js +602 -602
  137. package/lib/context-merge.js +359 -359
  138. package/lib/dep-guard/analyzer.js +294 -294
  139. package/lib/dep-guard/behavior-detector.js +98 -98
  140. package/lib/dep-guard/contract-detector.js +162 -162
  141. package/lib/dep-guard/import-detector.js +498 -498
  142. package/lib/dep-guard/path-utils.js +13 -13
  143. package/lib/dep-guard/rubric.js +120 -120
  144. package/lib/dep-guard/task-parser.js +318 -318
  145. package/lib/detect-agent.js +191 -191
  146. package/lib/detect-worktree.js +47 -47
  147. package/lib/file-hash.js +26 -26
  148. package/lib/husky-migration.js +450 -0
  149. package/lib/lefthook-check.js +65 -0
  150. package/lib/pat-setup.js +207 -0
  151. package/lib/plugin-catalog.js +350 -350
  152. package/lib/plugin-manager.js +166 -166
  153. package/lib/plugin-recommender.js +141 -141
  154. package/lib/project-discovery.js +491 -491
  155. package/lib/setup-action-log.js +139 -139
  156. package/lib/setup-summary-renderer.js +106 -106
  157. package/lib/setup-utils.js +96 -0
  158. package/lib/setup.js +192 -192
  159. package/lib/smart-merge.js +64 -0
  160. package/lib/symlink-utils.js +81 -0
  161. package/lib/workflow-profiles.js +197 -197
  162. package/package.json +131 -128
  163. package/scripts/beads-context.sh +291 -0
  164. package/scripts/beads-context.test.js +563 -0
  165. package/scripts/behavioral-judge.sh +378 -0
  166. package/scripts/benchmark.js +85 -0
  167. package/scripts/branch-protection.js +183 -0
  168. package/scripts/check-agents.js +172 -0
  169. package/scripts/commitlint.js +42 -0
  170. package/scripts/conflict-detect.sh +323 -0
  171. package/scripts/dep-guard-analyze.js +71 -0
  172. package/scripts/dep-guard.sh +811 -0
  173. package/scripts/eval_win.py +249 -0
  174. package/scripts/file-index.sh +399 -0
  175. package/scripts/github-beads-sync/comment.mjs +64 -0
  176. package/scripts/github-beads-sync/config.mjs +148 -0
  177. package/scripts/github-beads-sync/github-api.mjs +131 -0
  178. package/scripts/github-beads-sync/index.mjs +332 -0
  179. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  180. package/scripts/github-beads-sync/mapping.mjs +78 -0
  181. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  182. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  183. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  184. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  185. package/scripts/github-beads-sync.config.json +26 -0
  186. package/scripts/improve-command.js +375 -0
  187. package/scripts/lib/eval-runner.js +229 -0
  188. package/scripts/lib/eval-schema.js +135 -0
  189. package/scripts/lib/eval-storage.js +78 -0
  190. package/scripts/lib/grading.js +203 -0
  191. package/scripts/lib/transcript-parser.js +63 -0
  192. package/scripts/lint.js +47 -0
  193. package/scripts/migrate-to-bun-test.js +412 -0
  194. package/scripts/run-command-eval.js +236 -0
  195. package/scripts/smart-status.sh +782 -0
  196. package/scripts/sync-commands.js +571 -0
  197. package/scripts/sync-utils.sh +460 -0
  198. package/scripts/test-dashboard.js +123 -0
  199. package/scripts/test.js +44 -0
  200. package/scripts/validate.sh +94 -0
  201. package/skills/parallel-deep-research/SKILL.md +108 -108
  202. package/skills/parallel-deep-research/evals/README.md +27 -27
  203. package/skills/parallel-deep-research/evals/evals.json +62 -62
  204. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  205. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  206. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  207. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  208. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  209. package/.cursor/hooks/state/continual-learning.json +0 -8
@@ -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');