forge-workflow 0.0.8 → 0.0.10

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 (135) hide show
  1. package/.claude/commands/dev.md +2 -2
  2. package/.claude/commands/plan.md +2 -2
  3. package/.claude/commands/premerge.md +2 -2
  4. package/.claude/commands/review.md +5 -2
  5. package/.claude/commands/ship.md +6 -5
  6. package/.claude/commands/status.md +4 -4
  7. package/.claude/rules/greptile-review-process.md +4 -4
  8. package/.cline/workflows/dev.md +2 -2
  9. package/.cline/workflows/plan.md +2 -2
  10. package/.cline/workflows/premerge.md +2 -2
  11. package/.cline/workflows/review.md +5 -2
  12. package/.cline/workflows/ship.md +6 -5
  13. package/.cline/workflows/status.md +4 -4
  14. package/.codex/skills/dev/SKILL.md +2 -2
  15. package/.codex/skills/plan/SKILL.md +2 -2
  16. package/.codex/skills/premerge/SKILL.md +2 -2
  17. package/.codex/skills/review/SKILL.md +5 -2
  18. package/.codex/skills/ship/SKILL.md +6 -5
  19. package/.codex/skills/status/SKILL.md +4 -4
  20. package/.cursor/commands/dev.md +2 -2
  21. package/.cursor/commands/plan.md +2 -2
  22. package/.cursor/commands/premerge.md +2 -2
  23. package/.cursor/commands/review.md +5 -2
  24. package/.cursor/commands/ship.md +6 -5
  25. package/.cursor/commands/status.md +4 -4
  26. package/.github/prompts/dev.prompt.md +2 -2
  27. package/.github/prompts/plan.prompt.md +2 -2
  28. package/.github/prompts/premerge.prompt.md +2 -2
  29. package/.github/prompts/review.prompt.md +5 -2
  30. package/.github/prompts/ship.prompt.md +6 -5
  31. package/.github/prompts/status.prompt.md +4 -4
  32. package/.github/workflows/beads-to-github.yml +44 -11
  33. package/.github/workflows/github-to-beads.yml +11 -8
  34. package/.kilocode/workflows/dev.md +2 -2
  35. package/.kilocode/workflows/plan.md +2 -2
  36. package/.kilocode/workflows/premerge.md +2 -2
  37. package/.kilocode/workflows/review.md +5 -2
  38. package/.kilocode/workflows/ship.md +6 -5
  39. package/.kilocode/workflows/status.md +4 -4
  40. package/.opencode/commands/dev.md +2 -2
  41. package/.opencode/commands/plan.md +2 -2
  42. package/.opencode/commands/premerge.md +2 -2
  43. package/.opencode/commands/review.md +5 -2
  44. package/.opencode/commands/ship.md +6 -5
  45. package/.opencode/commands/status.md +4 -4
  46. package/.roo/commands/dev.md +2 -2
  47. package/.roo/commands/plan.md +2 -2
  48. package/.roo/commands/premerge.md +2 -2
  49. package/.roo/commands/review.md +5 -2
  50. package/.roo/commands/ship.md +6 -5
  51. package/.roo/commands/status.md +4 -4
  52. package/AGENTS.md +10 -9
  53. package/CLAUDE.md +12 -0
  54. package/README.md +12 -6
  55. package/bin/forge.js +24 -8
  56. package/docs/BEADS_GITHUB_SYNC.md +32 -2
  57. package/docs/EXAMPLES.md +22 -22
  58. package/docs/ROADMAP.md +3 -3
  59. package/docs/TOOLCHAIN.md +150 -160
  60. package/lib/agents/codex.plugin.json +3 -0
  61. package/lib/agents-config.js +18 -12
  62. package/lib/beads-bootstrap.js +225 -0
  63. package/lib/beads-health-check.js +55 -10
  64. package/lib/beads-setup.js +104 -28
  65. package/lib/beads-sync-scaffold.js +11 -6
  66. package/lib/codex-skills.js +54 -1
  67. package/lib/commands/_issue.js +11 -1
  68. package/lib/commands/issues.js +49 -0
  69. package/lib/commands/plan.js +5 -2
  70. package/lib/commands/recommend.js +22 -1
  71. package/lib/commands/setup.js +247 -27
  72. package/lib/commands/ship.js +188 -5
  73. package/lib/commands/status.js +187 -19
  74. package/lib/commands/team.js +11 -1
  75. package/lib/commands/test.js +125 -25
  76. package/lib/commands/validate.js +232 -9
  77. package/lib/commands/worktree.js +27 -54
  78. package/lib/dep-guard/keyword-ripple.js +184 -0
  79. package/lib/detect-worktree.js +9 -10
  80. package/lib/forge-issues.js +326 -0
  81. package/lib/issue-sync/authority.js +100 -0
  82. package/lib/issue-sync/github-pull.js +184 -0
  83. package/lib/issue-sync/import-primitives.js +98 -0
  84. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  85. package/lib/issue-sync/link-store.js +292 -0
  86. package/lib/issue-sync/project-github.js +123 -0
  87. package/lib/issue-sync/reconcile.js +195 -0
  88. package/lib/issue-sync/schema.js +126 -0
  89. package/lib/lefthook-check.js +5 -2
  90. package/lib/project-memory.js +564 -0
  91. package/lib/runtime-health.js +100 -12
  92. package/lib/setup-action-log.js +2 -0
  93. package/lib/setup-summary-renderer.js +15 -11
  94. package/lib/smart-status/conflicts.js +205 -0
  95. package/lib/smart-status/scoring.js +177 -0
  96. package/lib/status/beads-snapshot.js +102 -0
  97. package/lib/status/presenter.js +65 -0
  98. package/lib/workflow/enforce-stage.js +14 -8
  99. package/lib/workflow/state-manager.js +349 -0
  100. package/package.json +12 -4
  101. package/scripts/beads-context.sh +124 -5
  102. package/scripts/beads-context.test.js +21 -4
  103. package/scripts/beads-migrate-to-dolt.sh +7 -0
  104. package/scripts/beads-upgrade-smoke.sh +263 -0
  105. package/scripts/behavioral-judge.sh +115 -11
  106. package/scripts/benchmark.js +349 -63
  107. package/scripts/dep-guard-analyze.js +52 -17
  108. package/scripts/dep-guard-keyword-ripple.js +29 -0
  109. package/scripts/dep-guard-render-review.js +86 -0
  110. package/scripts/dep-guard.sh +45 -222
  111. package/scripts/forge-team/lib/hooks.sh +1 -1
  112. package/scripts/forge-team/lib/sync-github.sh +160 -28
  113. package/scripts/forge-team/lib/verify.sh +1 -1
  114. package/scripts/forge-team/lib/workload.sh +56 -27
  115. package/scripts/forge-team/tests/sync-github.test.sh +195 -58
  116. package/scripts/forge-team/tests/workload.test.sh +35 -4
  117. package/scripts/github-beads-sync/index.mjs +122 -98
  118. package/scripts/github-beads-sync/mapping.mjs +54 -0
  119. package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
  120. package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
  121. package/scripts/github-beads-sync/run-bd.mjs +4 -2
  122. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  123. package/scripts/preflight.sh +181 -0
  124. package/scripts/smart-status-score.js +31 -0
  125. package/scripts/smart-status-sessions.js +51 -0
  126. package/scripts/smart-status.sh +84 -330
  127. package/scripts/sync-agentic-workflow.js +48 -0
  128. package/scripts/sync-utils.sh +39 -0
  129. package/scripts/test-ci-shard.js +244 -0
  130. package/scripts/test-dashboard.js +188 -52
  131. package/scripts/test-full-suite.js +186 -0
  132. package/scripts/test-profile.js +278 -0
  133. package/scripts/test.js +335 -38
  134. package/scripts/validate.js +143 -0
  135. package/scripts/validate.sh +18 -1
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+
6
+ const {
7
+ applyMergeTreeConflicts,
8
+ computeFileConflicts,
9
+ matchInProgressIssues,
10
+ parseWorktreePorcelain,
11
+ } = require('../lib/smart-status/conflicts.js');
12
+
13
+ function readPayload() {
14
+ const raw = fs.readFileSync(0, 'utf8');
15
+ if (!raw.trim()) {
16
+ return {};
17
+ }
18
+
19
+ return JSON.parse(raw);
20
+ }
21
+
22
+ function buildSessions(payload) {
23
+ let sessions = Array.isArray(payload.sessions)
24
+ ? payload.sessions
25
+ : matchInProgressIssues(
26
+ parseWorktreePorcelain(payload.worktreePorcelain || '', payload.baseBranch || 'master'),
27
+ Array.isArray(payload.inProgressIssues) ? payload.inProgressIssues : [],
28
+ );
29
+
30
+ if (payload.branchFiles) {
31
+ sessions = computeFileConflicts(sessions, payload.branchFiles);
32
+ }
33
+
34
+ if (payload.mergeTreeResults) {
35
+ sessions = applyMergeTreeConflicts(sessions, payload.mergeTreeResults);
36
+ }
37
+
38
+ return sessions;
39
+ }
40
+
41
+ function main() {
42
+ try {
43
+ const payload = readPayload();
44
+ process.stdout.write(JSON.stringify(buildSessions(payload)));
45
+ } catch (error) {
46
+ console.error(`smart-status-sessions: ${error.message}`);
47
+ process.exit(1);
48
+ }
49
+ }
50
+
51
+ main();
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # smart-status.sh Workflow intelligence scoring engine
2
+ # smart-status.sh — Workflow intelligence scoring engine
3
3
  #
4
4
  # Reads issues via `bd list --json --limit 0`, computes a composite score
5
5
  # for each issue, and outputs them sorted by score descending.
@@ -11,20 +11,21 @@
11
11
  # smart-status.sh [--json]
12
12
  #
13
13
  # Environment:
14
- # BD_CMD override the bd command (for testing with mocks)
15
- # GIT_CMD override the git command (for testing with mocks)
16
- # DEFAULT_BRANCH override the default branch name (default: auto-detect)
14
+ # BD_CMD — override the bd command (for testing with mocks)
15
+ # GIT_CMD — override the git command (for testing with mocks)
16
+ # DEFAULT_BRANCH — override the default branch name (default: auto-detect)
17
17
  #
18
18
  # Cross-platform: bash 3.2 compatible (no associative arrays, no mapfile).
19
19
  # OWASP A03: All variables quoted, sanitize() strips injection patterns.
20
20
 
21
21
  set -euo pipefail
22
22
 
23
- # ── Source cross-dev awareness scripts ────────────────────────────────
23
+ # ── Source cross-dev awareness scripts ────────────────────────────────
24
24
  # file-index.sh: file_index_read for reading file index entries
25
25
  # sync-utils.sh: get_session_identity for current developer identity
26
26
 
27
27
  _SMART_STATUS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ NODE_CMD="${NODE_CMD:-node}"
28
29
  if [ -f "$_SMART_STATUS_DIR/lib/sanitize.sh" ]; then
29
30
  source "$_SMART_STATUS_DIR/lib/sanitize.sh"
30
31
  fi
@@ -35,7 +36,7 @@ if [ -f "$_SMART_STATUS_DIR/sync-utils.sh" ]; then
35
36
  source "$_SMART_STATUS_DIR/sync-utils.sh"
36
37
  fi
37
38
 
38
- # ── Helpers ──────────────────────────────────────────────────────────────
39
+ # ── Helpers ──────────────────────────────────────────────────────────────
39
40
 
40
41
  if ! declare -F sanitize >/dev/null; then
41
42
  # Sanitize a string: strip shell-injection patterns (OWASP A03)
@@ -56,7 +57,7 @@ sanitize() {
56
57
  }
57
58
  fi
58
59
 
59
- # ── Dependency check ────────────────────────────────────────────────────
60
+ # ── Dependency check ────────────────────────────────────────────────────
60
61
 
61
62
  if ! command -v jq &>/dev/null; then
62
63
  echo "Error: jq is required but not found." >&2
@@ -68,16 +69,25 @@ if ! command -v jq &>/dev/null; then
68
69
  fi
69
70
 
70
71
  # Warn if jq < 1.6 (fromdateiso8601 and sub/2 require 1.6+)
71
- _jq_version="$(jq --version 2>/dev/null | sed 's/jq-//')" || true
72
+ # Strip CR here as well because Windows jq.exe (or a CRLF-emitting wrapper in
73
+ # tests) can affect the version probe before the jq() shim is defined below.
74
+ _jq_version="$(command jq --version 2>/dev/null | tr -d '\r' | sed 's/jq-//')" || true
72
75
  if [ -n "$_jq_version" ]; then
73
76
  _jq_major="${_jq_version%%.*}"
74
77
  _jq_minor="${_jq_version#*.}"; _jq_minor="${_jq_minor%%.*}"
75
78
  if [ "${_jq_major:-0}" -lt 1 ] || { [ "${_jq_major:-0}" -eq 1 ] && [ "${_jq_minor:-0}" -lt 6 ]; }; then
76
- echo "Warning: jq $_jq_version detected staleness features require jq 1.6+. Team Activity may not display." >&2
79
+ echo "Warning: jq $_jq_version detected - staleness features require jq 1.6+. Team Activity may not display." >&2
77
80
  fi
78
81
  fi
79
82
 
80
- # ── Configuration ───────────────────────────────────────────────────────
83
+ # On Windows/WSL we may be invoking jq.exe, which emits CRLF and breaks
84
+ # numeric comparisons like `[ "$count" -gt 0 ]`. Strip trailing CRs from
85
+ # all jq output after the dependency/version checks above.
86
+ jq() {
87
+ command jq "$@" | tr -d '\r'
88
+ }
89
+
90
+ # ── Configuration ───────────────────────────────────────────────────────
81
91
 
82
92
  BD="${BD_CMD:-bd}"
83
93
  GIT="${GIT_CMD:-git}"
@@ -94,7 +104,7 @@ else
94
104
  BASE_BRANCH="master"
95
105
  fi
96
106
 
97
- # Parse arguments (bash 3.2 compatible no associative arrays)
107
+ # Parse arguments (bash 3.2 compatible — no associative arrays)
98
108
  for arg in "$@"; do
99
109
  case "$arg" in
100
110
  --json) JSON_MODE=1 ;;
@@ -115,7 +125,7 @@ for arg in "$@"; do
115
125
  esac
116
126
  done
117
127
 
118
- # ── Fetch issues ────────────────────────────────────────────────────────
128
+ # ── Fetch issues ────────────────────────────────────────────────────────
119
129
 
120
130
  # Auto-recover beads database if Dolt server lost the database (e.g. branch
121
131
  # switch, fresh clone, server restart). Checks for the specific "database
@@ -129,10 +139,18 @@ fi
129
139
 
130
140
  _bd_list_err="$("$BD" list --json --limit 0 2>&1 >/dev/null || true)"
131
141
  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
142
  _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
143
+ _metadata_path="$_repo_root/.beads/metadata.json"
144
+ _bd_prefix=""
145
+ if [ -f "$_metadata_path" ]; then
146
+ if _metadata_prefix="$(jq -r '(.dolt_database // "") | strings' "$_metadata_path" 2>/dev/null)"; then
147
+ _bd_prefix="$(printf '%s' "$_metadata_prefix" | tr -d '\r')"
148
+ fi
149
+ fi
150
+ if [ -z "$_bd_prefix" ]; then
151
+ _bd_prefix="$(basename "$_repo_root" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]-' '-' | sed 's/^-//;s/-$//')"
152
+ fi
153
+ echo "Beads database not found - attempting auto-recovery..." >&2
136
154
  if "$BD" init --force --prefix "$_bd_prefix" >/dev/null 2>&1; then
137
155
  if [ -d "$_repo_root/.beads/backup" ] && ls "$_repo_root/.beads/backup"/*.jsonl >/dev/null 2>&1; then
138
156
  "$BD" backup restore >/dev/null 2>&1 && echo "Beads: restored from backup." >&2 || echo "Beads: backup restore failed." >&2
@@ -140,7 +158,7 @@ if printf '%s' "$_bd_list_err" | grep -qi "database.*not found"; then
140
158
  echo "Beads: initialized fresh (no backup found)." >&2
141
159
  fi
142
160
  else
143
- echo "Beads: auto-recovery failed run 'bd doctor' manually." >&2
161
+ echo "Beads: auto-recovery failed - run 'bd doctor' manually." >&2
144
162
  fi
145
163
  fi
146
164
 
@@ -156,7 +174,7 @@ if [ "$(printf '%s' "$ISSUES_JSON" | jq 'length')" = "0" ]; then
156
174
  exit 0
157
175
  fi
158
176
 
159
- # ── Identify epics and fetch their children ─────────────────────────────
177
+ # ── Identify epics and fetch their children ─────────────────────────────
160
178
 
161
179
  # Extract epic IDs (type == "epic")
162
180
  EPIC_IDS="$(printf '%s' "$ISSUES_JSON" | jq -r '.[] | select(.type == "epic") | .id')"
@@ -164,7 +182,7 @@ EPIC_IDS="$(printf '%s' "$ISSUES_JSON" | jq -r '.[] | select(.type == "epic") |
164
182
  # Build a JSON object mapping epic_id -> { closed, total }
165
183
  EPIC_STATS="{}"
166
184
  if [ -n "$EPIC_IDS" ]; then
167
- # Process each epic (bash 3.2 compatible read line by line)
185
+ # Process each epic (bash 3.2 compatible — read line by line)
168
186
  while IFS= read -r epic_id; do
169
187
  [ -z "$epic_id" ] && continue
170
188
  CHILDREN_JSON="$("$BD" children "$epic_id" --json 2>/dev/null || echo '[]')"
@@ -176,92 +194,16 @@ $EPIC_IDS
176
194
  EOF
177
195
  fi
178
196
 
179
- # ── Score issues with jq ────────────────────────────────────────────────
180
-
181
- SCORED_JSON="$(printf '%s' "$ISSUES_JSON" | jq --argjson epic_stats "$EPIC_STATS" '
182
- # First pass: build reverse dependency map (who does each issue unblock?)
183
- # For each issue X, find all issues Y where Y.dependencies[].depends_on_id == X.id
184
- (reduce .[] as $dep_issue ({};
185
- reduce ($dep_issue.dependencies // [] | .[]) as $dep (.;
186
- .[$dep.depends_on_id] += [$dep_issue.id]
187
- )
188
- )) as $dependents_map |
189
-
190
- [.[] | . as $issue |
191
-
192
- # Priority weight: P0=5, P1=4, P2=3, P3=2, P4=1, default=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
202
- else 1 end) as $priority_weight |
203
-
204
- # Unblock chain: dependent_count + 1 (min 1).
205
- # NOTE: dependent_count is the authoritative server-side count. The "Unblocks:"
206
- # annotation (computed below from local response) may show fewer items when
207
- # bd list omits closed or filtered-out issues. This is an intentional trade-off.
208
- (((.dependent_count // 0) + 1) | if . < 1 then 1 else . end) as $unblock_chain |
209
-
210
- # Type weight: bug=1.2, feature=1.0, task=0.8, default=1.0
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
215
- else 1.0 end) as $type_weight |
216
-
217
- # Status boost: in_progress=1.5, open=1.0, default=1.0
218
- (if .status == "in_progress" then 1.5
219
- elif .status == "open" then 1.0
220
- else 1.0 end) as $status_boost |
221
-
222
- # Epic proximity: if issue has parent_id that is an epic, compute
223
- # 1.0 + (closed_siblings / total_siblings) * 0.5
224
- (if (.parent_id // "") != "" and ($epic_stats[.parent_id] // null) != null then
225
- $epic_stats[.parent_id] as $es |
226
- if $es.total > 0 then
227
- 1.0 + (($es.closed / $es.total) * 0.5)
228
- else 1.0 end
229
- else 1.0 end) as $epic_proximity |
230
-
231
- # Staleness boost based on updated_at
232
- # 0-7d=1.0, 7-14d=1.1, 14-30d=1.2, 30+d=1.5
233
- # Truncate to first 19 chars (YYYY-MM-DDTHH:MM:SS) + "Z" for fromdateiso8601.
234
- # Handles fractional seconds (.NNN), timezone offsets (+05:30), or both.
235
- (if .updated_at then
236
- ((.updated_at[:19] + "Z") | fromdateiso8601) as $ts |
237
- ((now - $ts) / 86400) as $days |
238
- if $days >= 30 then 1.5
239
- elif $days >= 14 then 1.2
240
- elif $days >= 7 then 1.1
241
- else 1.0 end
242
- else 1.0 end) as $staleness_boost |
243
-
244
- # Composite score
245
- ($priority_weight * $unblock_chain * $type_weight * $status_boost * $epic_proximity * $staleness_boost) as $score |
246
-
247
- # Compute dependents list (which issues does this one unblock?)
248
- ($dependents_map[$issue.id] // []) as $dependents_list |
249
-
250
- # Output scored issue
251
- $issue + {
252
- score: ($score * 100 | round / 100),
253
- priority_weight: $priority_weight,
254
- unblock_chain: $unblock_chain,
255
- type_weight: $type_weight,
256
- status_boost: $status_boost,
257
- epic_proximity: (($epic_proximity * 100 | round) / 100),
258
- staleness_boost: $staleness_boost,
259
- dependents: $dependents_list
260
- }
261
- ] | sort_by(-.score)
262
- ')"
263
-
264
- # ── Session detection ─────────────────────────────────────────────────
197
+ # ── Score issues via shared JS helper ──────────────────────────────────
198
+
199
+ if ! command -v "$NODE_CMD" >/dev/null 2>&1; then
200
+ echo "Error: node is required but not found." >&2
201
+ exit 1
202
+ fi
203
+
204
+ SCORED_JSON="$(printf '{"issues":%s,"epicStats":%s}' "$ISSUES_JSON" "$EPIC_STATS" | "$NODE_CMD" "$_SMART_STATUS_DIR/smart-status-score.js")"
205
+
206
+ # ── Session detection ─────────────────────────────────────────────────
265
207
 
266
208
  # Parse git worktree list --porcelain to find active sessions
267
209
  # Format: blocks separated by blank lines, each block has:
@@ -271,198 +213,51 @@ SCORED_JSON="$(printf '%s' "$ISSUES_JSON" | jq --argjson epic_stats "$EPIC_STATS
271
213
 
272
214
  WORKTREE_PORCELAIN="$("$GIT" worktree list --porcelain 2>/dev/null || echo '')"
273
215
 
274
- # Collect non-default-branch worktree branches and paths
275
- # Stored as newline-delimited "branch<TAB>path" entries — safe for paths with |, spaces, etc.
276
- SESSION_ENTRIES=""
277
- SESSION_COUNT=0
278
-
279
- _wt_path=""
280
- _wt_branch=""
281
- while IFS= read -r line; do
282
- case "$line" in
283
- worktree\ *)
284
- _wt_path="${line#worktree }"
285
- ;;
286
- branch\ *)
287
- _wt_branch="${line#branch refs/heads/}"
288
- ;;
289
- "")
290
- # End of block — process if we have a branch that is not the default branch
291
- if [ -n "$_wt_branch" ] && [ "$_wt_branch" != "$BASE_BRANCH" ]; then
292
- SESSION_COUNT=$((SESSION_COUNT + 1))
293
- if [ -n "$SESSION_ENTRIES" ]; then
294
- SESSION_ENTRIES="${SESSION_ENTRIES}
295
- ${_wt_branch} ${_wt_path}"
296
- else
297
- SESSION_ENTRIES="${_wt_branch} ${_wt_path}"
298
- fi
299
- fi
300
- _wt_path=""
301
- _wt_branch=""
302
- ;;
303
- esac
304
- done <<EOF
305
- ${WORKTREE_PORCELAIN}
306
216
 
307
- EOF
308
-
309
- # Get in-progress issues for matching (only if we have sessions)
217
+ # Build session/conflict data with the shared JS helper so the shell layer
218
+ # only performs command execution and rendering.
310
219
  IN_PROGRESS_JSON="[]"
311
- if [ "$SESSION_COUNT" -gt 0 ]; then
220
+ if [ -n "$WORKTREE_PORCELAIN" ]; then
312
221
  IN_PROGRESS_JSON="$("$BD" list --status in_progress --json --limit 0 2>/dev/null || echo '')"
313
- # Fallback: only if bd command failed (empty output), not if result is legitimately empty array
314
222
  if [ -z "$IN_PROGRESS_JSON" ] || ! printf '%s' "$IN_PROGRESS_JSON" | jq empty 2>/dev/null; then
315
223
  IN_PROGRESS_JSON="$(printf '%s' "$ISSUES_JSON" | jq '[.[] | select(.status == "in_progress")]')"
316
224
  fi
317
225
  fi
318
226
 
319
- # Build sessions data: for each branch, find matching in-progress issues
320
- # Match by: branch slug (after feat/, fix/, docs/) appears in issue title (case-insensitive, hyphen-to-space)
321
- SESSIONS_JSON="[]"
322
- if [ "$SESSION_COUNT" -gt 0 ]; then
323
- # Process each session (newline-delimited "branch<TAB>path" entries)
324
- while IFS=' ' read -r _branch _path; do
325
- [ -z "$_branch" ] && continue
227
+ SESSIONS_JSON="$(printf '{"baseBranch":%s,"worktreePorcelain":%s,"inProgressIssues":%s}' \
228
+ "$(printf '%s' "$BASE_BRANCH" | jq -R '.')" \
229
+ "$(printf '%s' "$WORKTREE_PORCELAIN" | jq -Rs '.')" \
230
+ "$IN_PROGRESS_JSON" | "$NODE_CMD" "$_SMART_STATUS_DIR/smart-status-sessions.js")"
231
+ SESSION_COUNT="$(printf '%s' "$SESSIONS_JSON" | jq 'length')"
326
232
 
327
- # Extract slug from branch name (strip any <prefix>/ convention)
328
- _slug="$_branch"
329
- case "$_slug" in
330
- */*) _slug="${_slug##*/}" ;;
331
- esac
332
-
333
- # Convert slug hyphens to spaces for title matching
334
- _slug_spaces="$(printf '%s' "$_slug" | tr '-' ' ')"
335
-
336
- # Find matching in-progress issues: title contains slug words (case-insensitive)
337
- _matched_ids="$(printf '%s' "$IN_PROGRESS_JSON" | jq -r --arg slug "$_slug_spaces" '
338
- [.[] | select(
339
- (.title | ascii_downcase | contains($slug | ascii_downcase))
340
- ) | .id] | join(",")
341
- ')"
342
-
343
- _issue_count=0
344
- if [ -n "$_matched_ids" ]; then
345
- # Count commas + 1
346
- _issue_count="$(printf '%s' "$_matched_ids" | tr -cd ',' | wc -c)"
347
- _issue_count=$((_issue_count + 1))
348
- fi
349
-
350
- # Build session JSON entry
351
- if [ -n "$_matched_ids" ]; then
352
- _ids_json="$(printf '%s' "$_matched_ids" | jq -R 'split(",")')"
353
- else
354
- _ids_json="[]"
355
- fi
356
-
357
- SESSIONS_JSON="$(printf '%s' "$SESSIONS_JSON" | jq \
358
- --arg branch "$_branch" \
359
- --arg path "$_path" \
360
- --argjson ids "$_ids_json" \
361
- --argjson count "$_issue_count" \
362
- '. + [{branch: $branch, path: $path, issue_ids: $ids, issue_count: $count}]'
363
- )"
364
-
365
- done <<SESSIONS_EOF
366
- $SESSION_ENTRIES
367
- SESSIONS_EOF
368
- fi
369
-
370
- # ── File-level conflict detection ────────────────────────────────────────
371
- # For each active worktree branch, get changed files via:
372
- # git diff <base-branch>...<branch> --name-only --
373
- # Uses -- separator to prevent argument injection (OWASP A03)
374
-
375
- # ALL_BRANCH_FILES: newline-delimited list of "branch<TAB>file1<TAB>file2..."
376
- ALL_BRANCH_FILES=""
377
233
  if [ "$SESSION_COUNT" -ge 2 ]; then
378
- # Read branches from SESSION_ENTRIES (newline-delimited "branch<TAB>path")
379
- while IFS=' ' read -r _branch _path; do
234
+ BRANCH_FILES_JSON="[]"
235
+ _session_branches="$(printf '%s' "$SESSIONS_JSON" | jq -r '.[].branch')"
236
+ while IFS= read -r _branch; do
380
237
  [ -z "$_branch" ] && continue
381
-
382
- # Get changed files for this branch vs BASE_BRANCH (-- prevents injection)
383
238
  _files="$("$GIT" diff "${BASE_BRANCH}...${_branch}" --name-only -- 2>/dev/null || echo '')"
384
- # Collapse to tab-delimited (tabs can't appear in filenames), strip empty lines
385
- _files_csv="$(printf '%s' "$_files" | tr '\n' '\t' | sed 's/\t$//' | sed 's/^\t//')"
386
-
387
- if [ -n "$ALL_BRANCH_FILES" ]; then
388
- ALL_BRANCH_FILES="${ALL_BRANCH_FILES}
389
- ${_branch}:${_files_csv}"
390
- else
391
- ALL_BRANCH_FILES="${_branch}:${_files_csv}"
392
- fi
393
- done <<CONFLICT_EOF
394
- $SESSION_ENTRIES
395
- CONFLICT_EOF
396
-
397
- # Build conflict map: find files that appear in multiple branches
398
- # and add changed_files + conflicts to SESSIONS_JSON
399
- while IFS= read -r _entry; do
400
- [ -z "$_entry" ] && continue
401
- _branch="${_entry%%:*}"
402
- _files_csv="${_entry#*:}"
403
-
404
- # Convert tab-delimited files to JSON array
405
- if [ -n "$_files_csv" ]; then
406
- _files_json="$(printf '%s' "$_files_csv" | jq -R 'split("\t")')"
407
- else
408
- _files_json="[]"
409
- fi
410
-
411
- # Find conflicts: files in this branch that also appear in other branches
412
- _conflicts_json="[]"
413
- while IFS= read -r _other_entry; do
414
- [ -z "$_other_entry" ] && continue
415
- _other_branch="${_other_entry%%:*}"
416
- _other_files="${_other_entry#*:}"
417
-
418
- # Skip self
419
- [ "$_other_branch" = "$_branch" ] && continue
420
- [ -z "$_other_files" ] && continue
421
-
422
- # Find intersection
423
- _other_json="$(printf '%s' "$_other_files" | jq -R 'split("\t")')"
424
- _overlap="$(jq -n --argjson a "$_files_json" --argjson b "$_other_json" \
425
- '[$a[] as $f | select($b | index($f))] | unique')"
426
- _overlap_len="$(printf '%s' "$_overlap" | jq 'length')"
427
- if [ "$_overlap_len" -gt 0 ]; then
428
- _conflicts_json="$(jq -n \
429
- --argjson existing "$_conflicts_json" \
430
- --arg other_branch "$_other_branch" \
431
- --argjson files "$_overlap" \
432
- '$existing + [{branch: $other_branch, files: $files}]')"
433
- fi
434
- done <<INNER_EOF
435
- $ALL_BRANCH_FILES
436
- INNER_EOF
437
-
438
- # Update SESSIONS_JSON: add changed_files and conflicts to matching branch entry
439
- SESSIONS_JSON="$(printf '%s' "$SESSIONS_JSON" | jq \
239
+ _files_json="$(printf '%s' "$_files" | jq -R -s 'split("\n") | map(select(length > 0))')"
240
+ BRANCH_FILES_JSON="$(printf '%s' "$BRANCH_FILES_JSON" | jq \
440
241
  --arg branch "$_branch" \
441
242
  --argjson files "$_files_json" \
442
- --argjson conflicts "$_conflicts_json" \
443
- '[.[] | if .branch == $branch then . + {changed_files: $files, conflicts: $conflicts} else . end]'
243
+ '. + [{branch: $branch, files: $files}]'
444
244
  )"
445
- done <<OUTER_EOF
446
- $ALL_BRANCH_FILES
447
- OUTER_EOF
448
- fi
245
+ done <<BRANCHEOF
246
+ $_session_branches
247
+ BRANCHEOF
449
248
 
450
- # ── Tier 2: git merge-tree conflict detection ──────────────────────────
451
- # If git >= 2.38 and there are file overlaps from Tier 1, run merge-tree
452
- # to detect actual merge conflicts vs mere file overlap.
249
+ SESSIONS_JSON="$(printf '{"sessions":%s,"branchFiles":%s}' "$SESSIONS_JSON" "$BRANCH_FILES_JSON" | "$NODE_CMD" "$_SMART_STATUS_DIR/smart-status-sessions.js")"
250
+ fi
453
251
 
454
252
  TIER2_ENABLED=0
455
253
  if [ "$SESSION_COUNT" -ge 2 ]; then
456
- # Check git version >= 2.38 (merge-tree --write-tree requires it)
457
254
  _git_ver="$("$GIT" --version 2>/dev/null || echo '')"
458
255
  _git_major=0
459
256
  _git_minor=0
460
- # Parse "git version X.Y.Z..." -> extract major and minor
461
257
  _ver_nums="${_git_ver#git version }"
462
258
  _git_major="${_ver_nums%%.*}"
463
259
  _ver_rest="${_ver_nums#*.}"
464
260
  _git_minor="${_ver_rest%%.*}"
465
- # Validate they are numbers
466
261
  case "$_git_major" in ''|*[!0-9]*) _git_major=0 ;; esac
467
262
  case "$_git_minor" in ''|*[!0-9]*) _git_minor=0 ;; esac
468
263
 
@@ -471,79 +266,38 @@ if [ "$SESSION_COUNT" -ge 2 ]; then
471
266
  fi
472
267
  fi
473
268
 
474
- # For each pair of branches with file overlaps, run merge-tree
475
269
  if [ "$TIER2_ENABLED" = "1" ]; then
476
- # Collect all branch pairs that have conflicts in SESSIONS_JSON
477
270
  _conflict_pairs="$(printf '%s' "$SESSIONS_JSON" | jq -r '
478
271
  [.[] | select((.conflicts // []) | length > 0) |
479
272
  .branch as $b |
480
273
  .conflicts[] |
481
- [$b, .branch] | sort | join(" ")
274
+ [$b, .branch] | sort | join("\t")
482
275
  ] | unique | .[]
483
276
  ')"
484
277
 
485
278
  if [ -n "$_conflict_pairs" ]; then
486
- # Process each unique pair
487
- while IFS= read -r _pair; do
488
- [ -z "$_pair" ] && continue
489
- _b1="${_pair%% *}"
490
- _b2="${_pair#* }"
491
-
492
- # Run merge-tree; exit 1 = actual conflict, exit 0 = clean merge
279
+ MERGE_TREE_RESULTS_JSON="[]"
280
+ while IFS=' ' read -r _b1 _b2; do
281
+ [ -z "$_b1" ] && continue
282
+ [ -z "$_b2" ] && continue
493
283
  _mt_output=""
494
284
  _mt_exit=0
495
285
  _mt_output="$("$GIT" merge-tree --write-tree --name-only --no-messages -- "$_b1" "$_b2" 2>/dev/null)" || _mt_exit=$?
496
-
497
- if [ "$_mt_exit" -ne 0 ] && [ -n "$_mt_output" ]; then
498
- # Real conflict detected — parse conflicted file names
499
- # merge-tree outputs a tree SHA on first line, then filenames
500
- # With --name-only, conflicted files are listed after the SHA line
501
- _conflict_files=""
502
- _line_num=0
503
- while IFS= read -r _cf_line; do
504
- _line_num=$((_line_num + 1))
505
- # Skip the first line (tree SHA output from merge-tree)
506
- if [ "$_line_num" -eq 1 ]; then continue; fi
507
- # Skip empty lines
508
- if [ -z "$_cf_line" ]; then continue; fi
509
- if [ -n "$_conflict_files" ]; then
510
- _conflict_files="${_conflict_files} ${_cf_line}"
511
- else
512
- _conflict_files="${_cf_line}"
513
- fi
514
- done <<MTEOF
515
- $_mt_output
516
- MTEOF
517
-
518
- if [ -n "$_conflict_files" ]; then
519
- _cf_json="$(printf '%s' "$_conflict_files" | jq -R 'split("\t")')"
520
-
521
- # Add merge_conflicts to both branches in SESSIONS_JSON
522
- for _target_branch in "$_b1" "$_b2"; do
523
- _other_branch="$_b1"
524
- [ "$_target_branch" = "$_b1" ] && _other_branch="$_b2"
525
-
526
- SESSIONS_JSON="$(printf '%s' "$SESSIONS_JSON" | jq \
527
- --arg branch "$_target_branch" \
528
- --arg other "$_other_branch" \
529
- --argjson files "$_cf_json" \
530
- '[.[] | if .branch == $branch then
531
- . + {merge_conflicts: ((.merge_conflicts // []) + [{branch: $other, files: $files}])}
532
- else . end]'
533
- )"
534
- done
535
- fi
536
- fi
286
+ MERGE_TREE_RESULTS_JSON="$(printf '%s' "$MERGE_TREE_RESULTS_JSON" | jq \
287
+ --arg left "$_b1" \
288
+ --arg right "$_b2" \
289
+ --arg output "$_mt_output" \
290
+ --argjson exitCode "$_mt_exit" \
291
+ '. + [{left: $left, right: $right, exitCode: $exitCode, output: $output}]'
292
+ )"
537
293
  done <<PAIRSEOF
538
294
  $_conflict_pairs
539
295
  PAIRSEOF
296
+
297
+ SESSIONS_JSON="$(printf '{"sessions":%s,"mergeTreeResults":%s}' "$SESSIONS_JSON" "$MERGE_TREE_RESULTS_JSON" | "$NODE_CMD" "$_SMART_STATUS_DIR/smart-status-sessions.js")"
540
298
  fi
541
299
  fi
542
300
 
543
- # ── Cross-developer team activity ────────────────────────────────────────
544
- # Read file index entries from all developers, identify OTHER developers'
545
- # in-progress work, compute module overlaps, and flag stale claims.
546
-
547
301
  TEAM_ACTIVITY_JSON="[]"
548
302
 
549
303
  # Only proceed if file_index_read and get_session_identity are available
@@ -611,7 +365,7 @@ if command -v file_index_read &>/dev/null && command -v get_session_identity &>/
611
365
  fi
612
366
  fi
613
367
 
614
- # ── Output ──────────────────────────────────────────────────────────────
368
+ # ── Output ──────────────────────────────────────────────────────────────
615
369
 
616
370
  if [ "$JSON_MODE" = "1" ]; then
617
371
  # Always output consistent shape: {sessions: [...], issues: [...], team_activity: [...]}
@@ -628,7 +382,7 @@ else
628
382
  C_RED=$'\033[31m' C_DIM=$'\033[2m' C_BOLD=$'\033[1m' C_CYAN=$'\033[36m'
629
383
  fi
630
384
 
631
- # ── Active Sessions (before grouped output) ──
385
+ # ── Active Sessions (before grouped output) ──
632
386
  if [ "$SESSION_COUNT" -gt 0 ]; then
633
387
  printf '%s%s=== ACTIVE SESSIONS ===%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET"
634
388
  printf '%s' "$SESSIONS_JSON" | jq -r '.[] |
@@ -678,10 +432,10 @@ else
678
432
  printf '\n'
679
433
  fi
680
434
 
681
- # ── Team Activity (cross-developer visibility) ──
435
+ # ── Team Activity (cross-developer visibility) ──
682
436
  _ta_len="$(printf '%s' "$TEAM_ACTIVITY_JSON" | jq 'length')"
683
437
  if [ "$_ta_len" -gt 0 ]; then
684
- printf '%s%s── Team Activity ──────────────────────────────────────%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET"
438
+ printf '%s%s--- Team Activity ----------------------------------------%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET"
685
439
  printf '%s' "$TEAM_ACTIVITY_JSON" | jq -r '
686
440
  .[] |
687
441
  .developer as $dev |
@@ -698,7 +452,7 @@ else
698
452
  else
699
453
  " " + $iid + " (in_progress)" +
700
454
  (if ($mods | length) > 0 then
701
- " touching " + ($mods | join(", "))
455
+ " - touching " + ($mods | join(", "))
702
456
  else "" end)
703
457
  end),
704
458
  # Overlap or no-overlap line