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