forge-workflow 0.0.5 → 0.0.7

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 (164) hide show
  1. package/.claude/commands/dev.md +6 -1
  2. package/.claude/commands/plan.md +59 -14
  3. package/.claude/commands/premerge.md +10 -0
  4. package/.claude/commands/review.md +7 -1
  5. package/.claude/commands/ship.md +95 -47
  6. package/.claude/commands/status.md +42 -0
  7. package/.claude/commands/validate.md +7 -1
  8. package/.claude/commands/verify.md +52 -4
  9. package/.claude/rules/workflow.md +16 -0
  10. package/.claude/scripts/greptile-resolve.sh +32 -0
  11. package/.cline/workflows/dev.md +6 -1
  12. package/.cline/workflows/plan.md +59 -14
  13. package/.cline/workflows/premerge.md +10 -0
  14. package/.cline/workflows/review.md +7 -1
  15. package/.cline/workflows/ship.md +95 -47
  16. package/.cline/workflows/status.md +42 -0
  17. package/.cline/workflows/validate.md +7 -1
  18. package/.cline/workflows/verify.md +52 -4
  19. package/.codex/skills/dev/SKILL.md +6 -1
  20. package/.codex/skills/plan/SKILL.md +59 -14
  21. package/.codex/skills/premerge/SKILL.md +10 -0
  22. package/.codex/skills/review/SKILL.md +7 -1
  23. package/.codex/skills/ship/SKILL.md +95 -47
  24. package/.codex/skills/status/SKILL.md +42 -0
  25. package/.codex/skills/validate/SKILL.md +7 -1
  26. package/.codex/skills/verify/SKILL.md +52 -4
  27. package/.cursor/commands/dev.md +6 -1
  28. package/.cursor/commands/plan.md +59 -14
  29. package/.cursor/commands/premerge.md +10 -0
  30. package/.cursor/commands/review.md +7 -1
  31. package/.cursor/commands/ship.md +95 -47
  32. package/.cursor/commands/status.md +42 -0
  33. package/.cursor/commands/validate.md +7 -1
  34. package/.cursor/commands/verify.md +52 -4
  35. package/.cursorrules +149 -0
  36. package/.github/prompts/dev.prompt.md +6 -1
  37. package/.github/prompts/plan.prompt.md +59 -14
  38. package/.github/prompts/premerge.prompt.md +10 -0
  39. package/.github/prompts/review.prompt.md +7 -1
  40. package/.github/prompts/ship.prompt.md +95 -47
  41. package/.github/prompts/status.prompt.md +42 -0
  42. package/.github/prompts/validate.prompt.md +7 -1
  43. package/.github/prompts/verify.prompt.md +52 -4
  44. package/.kilocode/workflows/dev.md +6 -1
  45. package/.kilocode/workflows/plan.md +59 -14
  46. package/.kilocode/workflows/premerge.md +10 -0
  47. package/.kilocode/workflows/review.md +7 -1
  48. package/.kilocode/workflows/ship.md +95 -47
  49. package/.kilocode/workflows/status.md +42 -0
  50. package/.kilocode/workflows/validate.md +7 -1
  51. package/.kilocode/workflows/verify.md +52 -4
  52. package/.opencode/commands/dev.md +6 -1
  53. package/.opencode/commands/plan.md +59 -14
  54. package/.opencode/commands/premerge.md +10 -0
  55. package/.opencode/commands/review.md +7 -1
  56. package/.opencode/commands/ship.md +95 -47
  57. package/.opencode/commands/status.md +42 -0
  58. package/.opencode/commands/validate.md +7 -1
  59. package/.opencode/commands/verify.md +52 -4
  60. package/.roo/commands/dev.md +6 -1
  61. package/.roo/commands/plan.md +59 -14
  62. package/.roo/commands/premerge.md +10 -0
  63. package/.roo/commands/review.md +7 -1
  64. package/.roo/commands/ship.md +95 -47
  65. package/.roo/commands/status.md +42 -0
  66. package/.roo/commands/validate.md +7 -1
  67. package/.roo/commands/verify.md +52 -4
  68. package/AGENTS.md +97 -0
  69. package/CLAUDE.md +10 -0
  70. package/README.md +2 -2
  71. package/bin/forge-cmd.js +5 -1
  72. package/bin/forge-preflight.js +15 -2
  73. package/bin/forge.js +211 -9
  74. package/docs/ENHANCED_ONBOARDING.md +96 -86
  75. package/docs/ROADMAP.md +2 -2
  76. package/docs/TOOLCHAIN.md +23 -0
  77. package/docs/VALIDATION.md +1 -1
  78. package/lefthook.yml +11 -0
  79. package/lib/agents/README.md +46 -1
  80. package/lib/agents/cline.plugin.json +11 -4
  81. package/lib/agents/codex.plugin.json +2 -2
  82. package/lib/agents/copilot.plugin.json +5 -5
  83. package/lib/agents/cursor.plugin.json +1 -1
  84. package/lib/agents/kilocode.plugin.json +1 -1
  85. package/lib/agents/opencode.plugin.json +7 -4
  86. package/lib/agents/roo.plugin.json +10 -3
  87. package/lib/agents-config.js +129 -81
  88. package/lib/codex-skills.js +50 -0
  89. package/lib/commands/_registry.js +173 -0
  90. package/lib/commands/clean.js +181 -0
  91. package/lib/commands/commands-reset.js +147 -0
  92. package/lib/commands/dev.js +84 -0
  93. package/lib/commands/plan.js +18 -0
  94. package/lib/commands/push.js +196 -0
  95. package/lib/commands/recommend.js +1 -1
  96. package/lib/commands/setup.js +4295 -0
  97. package/lib/commands/ship.js +20 -0
  98. package/lib/commands/status.js +210 -44
  99. package/lib/commands/sync.js +71 -0
  100. package/lib/commands/team.js +37 -0
  101. package/lib/commands/test.js +207 -0
  102. package/lib/commands/validate.js +13 -0
  103. package/lib/commands/worktree.js +310 -0
  104. package/lib/detect-agent.js +38 -8
  105. package/lib/detection-utils.js +405 -0
  106. package/lib/docs-command.js +51 -0
  107. package/lib/docs-copy.js +50 -0
  108. package/lib/file-utils.js +260 -0
  109. package/lib/forge-context.js +42 -0
  110. package/lib/freshness-token.js +148 -0
  111. package/lib/frontmatter.js +79 -0
  112. package/lib/greptile-match.js +80 -0
  113. package/lib/husky-migration.js +113 -12
  114. package/lib/lefthook-check.js +27 -6
  115. package/lib/plugin-manager.js +225 -72
  116. package/lib/project-discovery.js +39 -5
  117. package/lib/reset.js +309 -0
  118. package/lib/runtime-health.js +305 -0
  119. package/lib/shell-utils.js +50 -0
  120. package/lib/task-ownership.js +117 -0
  121. package/lib/ui-utils.js +43 -0
  122. package/lib/validation-utils.js +163 -0
  123. package/lib/workflow/enforce-stage.js +179 -0
  124. package/lib/workflow/stages.js +201 -0
  125. package/lib/workflow/state.js +332 -0
  126. package/opencode.json +67 -0
  127. package/package.json +16 -6
  128. package/scripts/beads-context.sh +165 -22
  129. package/scripts/beads-context.test.js +5 -1
  130. package/scripts/check-agents.js +103 -0
  131. package/scripts/check-forge-token.js +98 -0
  132. package/scripts/conflict-detect.sh +2 -2
  133. package/scripts/dep-guard.sh +6 -28
  134. package/scripts/file-index.sh +117 -23
  135. package/scripts/forge-team/index.sh +86 -0
  136. package/scripts/forge-team/lib/agent-prompt.sh +52 -0
  137. package/scripts/forge-team/lib/claim.sh +256 -0
  138. package/scripts/forge-team/lib/dashboard.sh +341 -0
  139. package/scripts/forge-team/lib/epic.sh +332 -0
  140. package/scripts/forge-team/lib/hooks.sh +253 -0
  141. package/scripts/forge-team/lib/identity.sh +235 -0
  142. package/scripts/forge-team/lib/sync-github.sh +317 -0
  143. package/scripts/forge-team/lib/verify.sh +284 -0
  144. package/scripts/forge-team/lib/workload.sh +296 -0
  145. package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
  146. package/scripts/forge-team/tests/claim.test.sh +179 -0
  147. package/scripts/forge-team/tests/dashboard.test.sh +170 -0
  148. package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
  149. package/scripts/forge-team/tests/epic.test.sh +176 -0
  150. package/scripts/forge-team/tests/hooks.test.sh +239 -0
  151. package/scripts/forge-team/tests/identity.test.sh +176 -0
  152. package/scripts/forge-team/tests/integration.test.sh +371 -0
  153. package/scripts/forge-team/tests/sync-github.test.sh +209 -0
  154. package/scripts/forge-team/tests/verify.test.sh +314 -0
  155. package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
  156. package/scripts/forge-team/tests/workload.test.sh +209 -0
  157. package/scripts/lib/eval-runner.js +39 -0
  158. package/scripts/lib/jsonl-lock.sh +48 -0
  159. package/scripts/lib/sanitize.sh +116 -0
  160. package/scripts/pr-coordinator.sh +756 -0
  161. package/scripts/smart-status.sh +58 -21
  162. package/scripts/sync-commands.js +49 -20
  163. package/scripts/sync-utils.sh +24 -29
  164. package/scripts/test.js +18 -1
@@ -20,8 +20,24 @@
20
20
 
21
21
  set -euo pipefail
22
22
 
23
+ # ── Source cross-dev awareness scripts ────────────────────────────────
24
+ # file-index.sh: file_index_read for reading file index entries
25
+ # sync-utils.sh: get_session_identity for current developer identity
26
+
27
+ _SMART_STATUS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ if [ -f "$_SMART_STATUS_DIR/lib/sanitize.sh" ]; then
29
+ source "$_SMART_STATUS_DIR/lib/sanitize.sh"
30
+ fi
31
+ if [ -f "$_SMART_STATUS_DIR/file-index.sh" ]; then
32
+ source "$_SMART_STATUS_DIR/file-index.sh"
33
+ fi
34
+ if [ -f "$_SMART_STATUS_DIR/sync-utils.sh" ]; then
35
+ source "$_SMART_STATUS_DIR/sync-utils.sh"
36
+ fi
37
+
23
38
  # ── Helpers ──────────────────────────────────────────────────────────────
24
39
 
40
+ if ! declare -F sanitize >/dev/null; then
25
41
  # Sanitize a string: strip shell-injection patterns (OWASP A03)
26
42
  # Removes: double quotes, $(...), backticks, semicolons, and newlines
27
43
  sanitize() {
@@ -38,17 +54,6 @@ sanitize() {
38
54
  val="$(printf '%s' "$val" | tr '\n' ' ')"
39
55
  printf '%s' "$val"
40
56
  }
41
-
42
- # ── Source cross-dev awareness scripts ────────────────────────────────
43
- # file-index.sh: file_index_read for reading file index entries
44
- # sync-utils.sh: get_session_identity for current developer identity
45
-
46
- _SMART_STATUS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
47
- if [ -f "$_SMART_STATUS_DIR/file-index.sh" ]; then
48
- source "$_SMART_STATUS_DIR/file-index.sh"
49
- fi
50
- if [ -f "$_SMART_STATUS_DIR/sync-utils.sh" ]; then
51
- source "$_SMART_STATUS_DIR/sync-utils.sh"
52
57
  fi
53
58
 
54
59
  # ── Dependency check ────────────────────────────────────────────────────
@@ -112,6 +117,33 @@ done
112
117
 
113
118
  # ── Fetch issues ────────────────────────────────────────────────────────
114
119
 
120
+ # Auto-recover beads database if Dolt server lost the database (e.g. branch
121
+ # switch, fresh clone, server restart). Checks for the specific "database
122
+ # not found" error, runs bd init --force + bd backup restore, then retries.
123
+ # Capture stderr into variable (2>&1 redirects stderr to stdout for capture,
124
+ # then >/dev/null discards original stdout so only errors remain).
125
+ if ! command -v "$BD" >/dev/null 2>&1; then
126
+ echo "Error: bd is required but not found." >&2
127
+ exit 1
128
+ fi
129
+
130
+ _bd_list_err="$("$BD" list --json --limit 0 2>&1 >/dev/null || true)"
131
+ if printf '%s' "$_bd_list_err" | grep -qi "database.*not found"; then
132
+ # Derive prefix from repo root directory name (not pwd, which could be a subdirectory)
133
+ _repo_root="$("$GIT" rev-parse --show-toplevel 2>/dev/null || pwd)"
134
+ _bd_prefix="$(basename "$_repo_root" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]-' '-' | sed 's/^-//;s/-$//')"
135
+ echo "Beads database not found — attempting auto-recovery..." >&2
136
+ if "$BD" init --force --prefix "$_bd_prefix" >/dev/null 2>&1; then
137
+ if [ -d "$_repo_root/.beads/backup" ] && ls "$_repo_root/.beads/backup"/*.jsonl >/dev/null 2>&1; then
138
+ "$BD" backup restore >/dev/null 2>&1 && echo "Beads: restored from backup." >&2 || echo "Beads: backup restore failed." >&2
139
+ else
140
+ echo "Beads: initialized fresh (no backup found)." >&2
141
+ fi
142
+ else
143
+ echo "Beads: auto-recovery failed — run 'bd doctor' manually." >&2
144
+ fi
145
+ fi
146
+
115
147
  ISSUES_JSON="$("$BD" list --json --limit 0 2>/dev/null || echo '[]')"
116
148
 
117
149
  # Bail early on empty
@@ -158,11 +190,15 @@ SCORED_JSON="$(printf '%s' "$ISSUES_JSON" | jq --argjson epic_stats "$EPIC_STATS
158
190
  [.[] | . as $issue |
159
191
 
160
192
  # Priority weight: P0=5, P1=4, P2=3, P3=2, P4=1, default=1
161
- (if .priority == "P0" then 5
162
- elif .priority == "P1" then 4
163
- elif .priority == "P2" then 3
164
- elif .priority == "P3" then 2
165
- elif .priority == "P4" then 1
193
+ # Normalize priority to number (beads 0.62+ sends numbers, older sends strings)
194
+ ((.priority // 2) | if type == "string" then
195
+ (if . == "P0" then 0 elif . == "P1" then 1 elif . == "P2" then 2 elif . == "P3" then 3 elif . == "P4" then 4 else 5 end)
196
+ else . end) as $pri_num |
197
+ (if $pri_num == 0 then 5
198
+ elif $pri_num == 1 then 4
199
+ elif $pri_num == 2 then 3
200
+ elif $pri_num == 3 then 2
201
+ elif $pri_num == 4 then 1
166
202
  else 1 end) as $priority_weight |
167
203
 
168
204
  # Unblock chain: dependent_count + 1 (min 1).
@@ -172,9 +208,10 @@ SCORED_JSON="$(printf '%s' "$ISSUES_JSON" | jq --argjson epic_stats "$EPIC_STATS
172
208
  (((.dependent_count // 0) + 1) | if . < 1 then 1 else . end) as $unblock_chain |
173
209
 
174
210
  # Type weight: bug=1.2, feature=1.0, task=0.8, default=1.0
175
- (if .type == "bug" then 1.2
176
- elif .type == "feature" then 1.0
177
- elif .type == "task" then 0.8
211
+ # Handle null type from beads 0.62+
212
+ (if (.type // "task") == "bug" then 1.2
213
+ elif (.type // "task") == "feature" then 1.0
214
+ elif (.type // "task") == "task" then 0.8
178
215
  else 1.0 end) as $type_weight |
179
216
 
180
217
  # Status boost: in_progress=1.5, open=1.0, default=1.0
@@ -690,7 +727,7 @@ else
690
727
  (if .status == "in_progress" then "RESUME"
691
728
  elif (.dependent_count // 0) >= 2 then "UNBLOCK_CHAINS"
692
729
  elif (.dependency_count // 0) > 0 and .status != "closed" then "BLOCKED"
693
- elif .priority == "P4" then "BACKLOG"
730
+ elif (.priority == "P4" or .priority == 4) then "BACKLOG"
694
731
  else "READY_WORK"
695
732
  end) as $group |
696
733
  # Flag in_progress issues that have active blockers
@@ -734,7 +771,7 @@ else
734
771
  (if $item.blocked_resume then " !! BLOCKED by dependencies" else "" end) as $blocked_warn |
735
772
  "ENTRY:" + $item.group + ":" +
736
773
  $rank + ". [" + ($item.score | tostring) + "] " +
737
- $item.id + " (" + ($item.priority // "-") + " " + ($item.type // "-") + ") -- " +
774
+ $item.id + " (" + (if ($item.priority | type) == "number" then "P" + ($item.priority | tostring) else ($item.priority // "-" | tostring) end) + " " + ($item.type // "-" | tostring) + ") -- " +
738
775
  ($item.title // "-") + " " + $status_tag + $stale + $blocked_warn + $unblocks
739
776
  ),
740
777
  ""
@@ -12,6 +12,10 @@
12
12
  */
13
13
 
14
14
  const YAML = require('yaml');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const crypto = require('crypto');
18
+ const { listCodexSkillEntries } = require('../lib/codex-skills');
15
19
 
16
20
  /**
17
21
  * Parse YAML frontmatter from a markdown string.
@@ -293,9 +297,14 @@ function adaptForAgent(agentName, frontmatter, body, commandName) {
293
297
 
294
298
  // ---- Sync logic -----------------------------------------------------------------
295
299
 
296
- const fs = require('fs');
297
- const path = require('path');
298
- const crypto = require('crypto');
300
+ function resolveCanonicalCommandsDir(repoRoot) {
301
+ const candidates = [
302
+ path.join(repoRoot, 'commands'),
303
+ path.join(repoRoot, '.claude', 'commands'),
304
+ ];
305
+
306
+ return candidates.find(candidate => fs.existsSync(candidate)) || null;
307
+ }
299
308
 
300
309
  /**
301
310
  * Compute a content hash for change detection.
@@ -310,6 +319,25 @@ function contentHash(content) {
310
319
  return crypto.createHash('sha256').update(normalized).digest('hex');
311
320
  }
312
321
 
322
+ /**
323
+ * Write the sync manifest for generated entries without rewriting command files.
324
+ *
325
+ * @param {string} repoRoot
326
+ * @param {SyncEntry[]} entries
327
+ */
328
+ function writeSyncManifest(repoRoot, entries) {
329
+ const manifestDir = path.join(repoRoot, '.forge');
330
+ fs.mkdirSync(manifestDir, { recursive: true });
331
+ const manifestData = {
332
+ generatedAt: new Date().toISOString(),
333
+ files: entries.map((e) => path.relative(repoRoot, e.filePath).replace(/\\/g, '/')),
334
+ };
335
+ fs.writeFileSync(
336
+ path.join(manifestDir, 'sync-manifest.json'),
337
+ JSON.stringify(manifestData, null, 2) + '\n'
338
+ );
339
+ }
340
+
313
341
  /**
314
342
  * @typedef {Object} SyncEntry
315
343
  * @property {string} agent - Agent slug
@@ -339,11 +367,11 @@ function contentHash(content) {
339
367
  * - `check: true` — compares generated content with existing files, reports mismatches
340
368
  * - default (both false) — writes files, creating directories as needed
341
369
  *
342
- * @param {{ dryRun: boolean, check: boolean, repoRoot: string }} options
370
+ * @param {{ dryRun: boolean, check: boolean, repoRoot: string, canonicalDir?: string | null }} options
343
371
  * @returns {SyncResult}
344
372
  */
345
- function syncCommands({ dryRun, check, repoRoot }) {
346
- const commandsDir = path.join(repoRoot, '.claude', 'commands');
373
+ function syncCommands({ dryRun, check, repoRoot, canonicalDir = null }) {
374
+ const commandsDir = canonicalDir || resolveCanonicalCommandsDir(repoRoot) || path.join(repoRoot, '.claude', 'commands');
347
375
 
348
376
  // Read all .md files from the commands directory
349
377
  /** @type {string[]} */
@@ -474,16 +502,7 @@ function syncCommands({ dryRun, check, repoRoot }) {
474
502
 
475
503
  // Write sync manifest — records every file generated so stale detection
476
504
  // can identify orphaned files without false-flagging custom files.
477
- const manifestDir = path.join(repoRoot, '.forge');
478
- fs.mkdirSync(manifestDir, { recursive: true });
479
- const manifestData = {
480
- generatedAt: new Date().toISOString(),
481
- files: written.map((e) => path.relative(repoRoot, e.filePath).replace(/\\/g, '/')),
482
- };
483
- fs.writeFileSync(
484
- path.join(manifestDir, 'sync-manifest.json'),
485
- JSON.stringify(manifestData, null, 2) + '\n'
486
- );
505
+ writeSyncManifest(repoRoot, written);
487
506
 
488
507
  return { written, overwritten };
489
508
  }
@@ -506,7 +525,7 @@ if (require.main === module) {
506
525
 
507
526
  if (dryRun) {
508
527
  if (result.planned.length === 0) {
509
- console.log('No command files found in .claude/commands/');
528
+ console.log('No command files found in commands/ or .claude/commands/');
510
529
  } else {
511
530
  console.log('Dry run — files that would be generated:\n');
512
531
  for (const entry of result.planned) {
@@ -516,7 +535,7 @@ if (require.main === module) {
516
535
  }
517
536
  } else if (check) {
518
537
  if (result.empty) {
519
- console.error('Error: no command files found in .claude/commands/ — cannot verify sync.');
538
+ console.error('Error: no command files found in commands/ or .claude/commands/ — cannot verify sync.');
520
539
  process.exit(1);
521
540
  }
522
541
  if (result.manifestMissing) {
@@ -561,11 +580,21 @@ if (require.main === module) {
561
580
  }
562
581
 
563
582
  if (result.written.length === 0) {
564
- console.log('No command files found in .claude/commands/');
583
+ console.log('No command files found in commands/ or .claude/commands/');
565
584
  } else {
566
585
  console.log(`Synced ${result.written.length} file(s) across agents.`);
567
586
  }
568
587
  }
569
588
  }
570
589
 
571
- module.exports = { parseFrontmatter, buildFile, AGENT_ADAPTERS, adaptForAgent, syncCommands };
590
+ module.exports = {
591
+ parseFrontmatter,
592
+ buildFile,
593
+ AGENT_ADAPTERS,
594
+ adaptForAgent,
595
+ listCodexSkillEntries,
596
+ syncCommands,
597
+ contentHash,
598
+ resolveCanonicalCommandsDir,
599
+ writeSyncManifest,
600
+ };
@@ -11,6 +11,10 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
11
11
  set -euo pipefail
12
12
  fi
13
13
 
14
+ # Source shared sanitize library
15
+ _SU_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ source "$_SU_SCRIPT_DIR/lib/sanitize.sh"
17
+
14
18
  # ── Session Identity ───────────────────────────────────────────────────
15
19
 
16
20
  # Validates a session identity string against the allowlist regex.
@@ -76,28 +80,6 @@ get_session_identity() {
76
80
  printf '%s' "$identity"
77
81
  }
78
82
 
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
83
  # ── Config Reading ─────────────────────────────────────────────────────
102
84
 
103
85
  # Read a key from .beads/config.json (JSON format).
@@ -277,25 +259,37 @@ _SYNC_STALENESS_THRESHOLD=900
277
259
  # On success: records Unix epoch timestamp to .beads/.last-sync
278
260
  # and updates file index from all in-progress issues' task files.
279
261
  #
262
+ # _run_sync — execute beads sync (pull + push).
263
+ # If BD_SYNC_CMD is set, run it as a single command (for testing).
264
+ # Otherwise, run the default two-step pull+push sequence.
265
+ # shellcheck disable=SC2086
266
+ _run_sync() {
267
+ if [[ -n "${BD_SYNC_CMD:-}" ]]; then
268
+ $BD_SYNC_CMD
269
+ else
270
+ bd dolt pull && bd dolt push
271
+ fi
272
+ }
273
+
280
274
  # Environment overrides (for testing):
281
- # BD_SYNC_CMD — command to run instead of `bd sync` (default: "bd sync")
275
+ # BD_SYNC_CMD — single command to run instead of default pull+push (e.g. "echo mock-sync")
282
276
  # FILE_INDEX_ROOT — root directory for file-index.sh (default: repo_dir)
283
277
  auto_sync() {
284
278
  local repo_dir="${1:-.}"
285
- local sync_cmd="${BD_SYNC_CMD:-bd sync}"
286
279
  local last_sync_file="$repo_dir/.beads/.last-sync"
287
280
 
288
281
  # Ensure .beads directory exists
289
282
  mkdir -p "$repo_dir/.beads"
290
283
 
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
284
+ # Run sync — helper function avoids eval while supporting compound default
285
+ if _run_sync >/dev/null 2>&1; then
294
286
  # Success: record timestamp
295
287
  date +%s > "$last_sync_file"
296
288
 
297
289
  # Update file index from in-progress issues' task files
298
- _auto_sync_update_file_index "$repo_dir"
290
+ _auto_sync_update_file_index "$repo_dir" || {
291
+ echo "Warning: file index update failed after sync" >&2
292
+ }
299
293
  else
300
294
  # Failure: warn but continue (non-blocking)
301
295
  local last_ts="unknown"
@@ -340,7 +334,8 @@ _auto_sync_update_file_index() {
340
334
 
341
335
  # Check jq is available
342
336
  if ! command -v jq &>/dev/null; then
343
- return 0
337
+ echo "Warning: jq not found — file index update skipped" >&2
338
+ return 1
344
339
  fi
345
340
 
346
341
  # Extract in-progress issue IDs (LWW resolution: group by id, take last, filter in_progress)
package/scripts/test.js CHANGED
@@ -22,7 +22,24 @@ function detectPackageManager() {
22
22
  const pkgManager = detectPackageManager();
23
23
  console.log(`🧪 Running test suite (${pkgManager} test)...`);
24
24
 
25
- const result = spawnSync(pkgManager, ['test'], { stdio: 'inherit', shell: isWindows });
25
+ // Strip git hook environment variables so child processes (especially tests
26
+ // that create temp git repos) never accidentally operate on the real worktree.
27
+ // During pre-push hooks, git sets GIT_DIR pointing to the repo — any test that
28
+ // runs `git init` / `git commit` in a temp dir inherits this and silently
29
+ // commits into the worktree instead, creating rogue "initial commit" that
30
+ // deletes the entire codebase.
31
+ const env = { ...process.env };
32
+ for (const key of Object.keys(env)) {
33
+ if (key === 'GIT_DIR' || key === 'GIT_WORK_TREE' || key === 'GIT_INDEX_FILE'
34
+ || key === 'GIT_OBJECT_DIRECTORY' || key === 'GIT_ALTERNATE_OBJECT_DIRECTORIES'
35
+ || key === 'GIT_QUARANTINE_PATH') {
36
+ delete env[key];
37
+ }
38
+ }
39
+
40
+ // Use 'run test' to invoke the package.json script (which may include --timeout flags)
41
+ // 'bun test' is a built-in that ignores package.json scripts
42
+ const result = spawnSync(pkgManager, ['run', 'test'], { stdio: 'inherit', shell: isWindows, env });
26
43
 
27
44
  if (result.error) {
28
45
  console.error('');