forge-workflow 0.0.4 → 0.0.6
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.
- package/.claude/commands/dev.md +345 -340
- package/.claude/commands/plan.md +566 -521
- package/.claude/commands/premerge.md +186 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +448 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +212 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +90 -48
- package/.claude/commands/validate.md +288 -282
- package/.claude/commands/verify.md +269 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +121 -105
- package/.claude/scripts/greptile-resolve.sh +558 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +342 -337
- package/.cline/workflows/plan.md +563 -518
- package/.cline/workflows/premerge.md +183 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +445 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +209 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +87 -45
- package/.cline/workflows/validate.md +285 -279
- package/.cline/workflows/verify.md +266 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +345 -340
- package/.codex/skills/plan/SKILL.md +566 -521
- package/.codex/skills/premerge/SKILL.md +186 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +448 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +212 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +90 -48
- package/.codex/skills/validate/SKILL.md +288 -282
- package/.codex/skills/verify/SKILL.md +269 -221
- package/.cursor/commands/dev.md +342 -337
- package/.cursor/commands/plan.md +563 -518
- package/.cursor/commands/premerge.md +183 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +445 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +209 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +87 -45
- package/.cursor/commands/validate.md +285 -279
- package/.cursor/commands/verify.md +266 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +347 -342
- package/.github/prompts/plan.prompt.md +568 -523
- package/.github/prompts/premerge.prompt.md +188 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +450 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +214 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +92 -50
- package/.github/prompts/validate.prompt.md +290 -284
- package/.github/prompts/verify.prompt.md +271 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +346 -341
- package/.kilocode/workflows/plan.md +567 -522
- package/.kilocode/workflows/premerge.md +187 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +449 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +213 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +91 -49
- package/.kilocode/workflows/validate.md +289 -283
- package/.kilocode/workflows/verify.md +270 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +345 -340
- package/.opencode/commands/plan.md +566 -521
- package/.opencode/commands/premerge.md +186 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +448 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +212 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +90 -48
- package/.opencode/commands/validate.md +288 -282
- package/.opencode/commands/verify.md +269 -221
- package/.roo/commands/dev.md +346 -341
- package/.roo/commands/plan.md +567 -522
- package/.roo/commands/premerge.md +187 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +449 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +213 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +91 -49
- package/.roo/commands/validate.md +289 -283
- package/.roo/commands/verify.md +270 -222
- package/AGENTS.md +272 -175
- package/CLAUDE.md +110 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +317 -313
- package/bin/forge-preflight.js +322 -309
- package/bin/forge.js +4765 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +612 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -631
- package/docs/TOOLCHAIN.md +653 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +50 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +571 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +602 -602
- package/lib/commands/worktree.js +310 -0
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -191
- package/lib/detect-worktree.js +47 -47
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-hash.js +26 -26
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/reset.js +309 -0
- package/lib/setup-action-log.js +139 -139
- package/lib/setup-summary-renderer.js +106 -106
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -192
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/task-ownership.js +117 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +426 -0
- package/scripts/beads-context.test.js +567 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +789 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +493 -0
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +268 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/pr-coordinator.sh +706 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +809 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +455 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +46 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/.cursor/hooks/state/continual-learning-index.json +0 -19
- package/.cursor/hooks/state/continual-learning.json +0 -8
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# smart-status.sh — Workflow intelligence scoring engine
|
|
3
|
+
#
|
|
4
|
+
# Reads issues via `bd list --json --limit 0`, computes a composite score
|
|
5
|
+
# for each issue, and outputs them sorted by score descending.
|
|
6
|
+
#
|
|
7
|
+
# Composite score:
|
|
8
|
+
# priority_weight * unblock_chain * type_weight * status_boost * epic_proximity * staleness_boost
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# smart-status.sh [--json]
|
|
12
|
+
#
|
|
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)
|
|
17
|
+
#
|
|
18
|
+
# Cross-platform: bash 3.2 compatible (no associative arrays, no mapfile).
|
|
19
|
+
# OWASP A03: All variables quoted, sanitize() strips injection patterns.
|
|
20
|
+
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
# ── Helpers ──────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
# Sanitize a string: strip shell-injection patterns (OWASP A03)
|
|
26
|
+
# Removes: double quotes, $(...), backticks, semicolons, and newlines
|
|
27
|
+
sanitize() {
|
|
28
|
+
local val="$1"
|
|
29
|
+
# Remove double quotes
|
|
30
|
+
val="${val//\"/}"
|
|
31
|
+
# Remove $(...) command substitution patterns (loop handles nested)
|
|
32
|
+
val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
|
|
33
|
+
# Remove backtick command substitution
|
|
34
|
+
val="${val//\`/}"
|
|
35
|
+
# Remove semicolons (command chaining)
|
|
36
|
+
val="${val//;/}"
|
|
37
|
+
# Collapse newlines to spaces
|
|
38
|
+
val="$(printf '%s' "$val" | tr '\n' ' ')"
|
|
39
|
+
printf '%s' "$val"
|
|
40
|
+
}
|
|
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
|
+
fi
|
|
53
|
+
|
|
54
|
+
# ── Dependency check ────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
if ! command -v jq &>/dev/null; then
|
|
57
|
+
echo "Error: jq is required but not found." >&2
|
|
58
|
+
echo "Install jq:" >&2
|
|
59
|
+
echo " macOS: brew install jq" >&2
|
|
60
|
+
echo " Ubuntu: sudo apt-get install jq" >&2
|
|
61
|
+
echo " Windows: winget install jqlang.jq" >&2
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Warn if jq < 1.6 (fromdateiso8601 and sub/2 require 1.6+)
|
|
66
|
+
_jq_version="$(jq --version 2>/dev/null | sed 's/jq-//')" || true
|
|
67
|
+
if [ -n "$_jq_version" ]; then
|
|
68
|
+
_jq_major="${_jq_version%%.*}"
|
|
69
|
+
_jq_minor="${_jq_version#*.}"; _jq_minor="${_jq_minor%%.*}"
|
|
70
|
+
if [ "${_jq_major:-0}" -lt 1 ] || { [ "${_jq_major:-0}" -eq 1 ] && [ "${_jq_minor:-0}" -lt 6 ]; }; then
|
|
71
|
+
echo "Warning: jq $_jq_version detected — staleness features require jq 1.6+. Team Activity may not display." >&2
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# ── Configuration ───────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
BD="${BD_CMD:-bd}"
|
|
78
|
+
GIT="${GIT_CMD:-git}"
|
|
79
|
+
JSON_MODE=0
|
|
80
|
+
|
|
81
|
+
# Auto-detect default branch (master or main), with override via DEFAULT_BRANCH env
|
|
82
|
+
if [ -n "${DEFAULT_BRANCH:-}" ]; then
|
|
83
|
+
BASE_BRANCH="$DEFAULT_BRANCH"
|
|
84
|
+
elif "$GIT" rev-parse --verify master >/dev/null 2>&1; then
|
|
85
|
+
BASE_BRANCH="master"
|
|
86
|
+
elif "$GIT" rev-parse --verify main >/dev/null 2>&1; then
|
|
87
|
+
BASE_BRANCH="main"
|
|
88
|
+
else
|
|
89
|
+
BASE_BRANCH="master"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Parse arguments (bash 3.2 compatible — no associative arrays)
|
|
93
|
+
for arg in "$@"; do
|
|
94
|
+
case "$arg" in
|
|
95
|
+
--json) JSON_MODE=1 ;;
|
|
96
|
+
--help|-h)
|
|
97
|
+
echo "Usage: smart-status.sh [--json]"
|
|
98
|
+
echo ""
|
|
99
|
+
echo "Scores and ranks issues by composite priority score."
|
|
100
|
+
echo ""
|
|
101
|
+
echo "Options:"
|
|
102
|
+
echo " --json Output raw scored JSON array"
|
|
103
|
+
echo " --help Show this help message"
|
|
104
|
+
exit 0
|
|
105
|
+
;;
|
|
106
|
+
*)
|
|
107
|
+
echo "Error: unknown argument: $(sanitize "$arg")" >&2
|
|
108
|
+
exit 1
|
|
109
|
+
;;
|
|
110
|
+
esac
|
|
111
|
+
done
|
|
112
|
+
|
|
113
|
+
# ── Fetch issues ────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
# Auto-recover beads database if Dolt server lost the database (e.g. branch
|
|
116
|
+
# switch, fresh clone, server restart). Checks for the specific "database
|
|
117
|
+
# not found" error, runs bd init --force + bd backup restore, then retries.
|
|
118
|
+
# Capture stderr into variable (2>&1 redirects stderr to stdout for capture,
|
|
119
|
+
# then >/dev/null discards original stdout so only errors remain).
|
|
120
|
+
_bd_list_err="$("$BD" list --json --limit 0 2>&1 >/dev/null || true)"
|
|
121
|
+
if printf '%s' "$_bd_list_err" | grep -qi "database.*not found"; then
|
|
122
|
+
# Derive prefix from repo root directory name (not pwd, which could be a subdirectory)
|
|
123
|
+
_repo_root="$("$GIT" rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
124
|
+
_bd_prefix="$(basename "$_repo_root" | tr '[:upper:]' '[:lower:]' | tr -cs '[:alnum:]-' '-' | sed 's/^-//;s/-$//')"
|
|
125
|
+
echo "Beads database not found — attempting auto-recovery..." >&2
|
|
126
|
+
if "$BD" init --force --prefix "$_bd_prefix" >/dev/null 2>&1; then
|
|
127
|
+
if [ -d "$_repo_root/.beads/backup" ] && ls "$_repo_root/.beads/backup"/*.jsonl >/dev/null 2>&1; then
|
|
128
|
+
"$BD" backup restore >/dev/null 2>&1 && echo "Beads: restored from backup." >&2 || echo "Beads: backup restore failed." >&2
|
|
129
|
+
else
|
|
130
|
+
echo "Beads: initialized fresh (no backup found)." >&2
|
|
131
|
+
fi
|
|
132
|
+
else
|
|
133
|
+
echo "Beads: auto-recovery failed — run 'bd doctor' manually." >&2
|
|
134
|
+
fi
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
ISSUES_JSON="$("$BD" list --json --limit 0 2>/dev/null || echo '[]')"
|
|
138
|
+
|
|
139
|
+
# Bail early on empty
|
|
140
|
+
if [ "$(printf '%s' "$ISSUES_JSON" | jq 'length')" = "0" ]; then
|
|
141
|
+
if [ "$JSON_MODE" = "1" ]; then
|
|
142
|
+
echo '{"sessions":[],"issues":[],"team_activity":[]}'
|
|
143
|
+
else
|
|
144
|
+
echo "No issues found."
|
|
145
|
+
fi
|
|
146
|
+
exit 0
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
# ── Identify epics and fetch their children ─────────────────────────────
|
|
150
|
+
|
|
151
|
+
# Extract epic IDs (type == "epic")
|
|
152
|
+
EPIC_IDS="$(printf '%s' "$ISSUES_JSON" | jq -r '.[] | select(.type == "epic") | .id')"
|
|
153
|
+
|
|
154
|
+
# Build a JSON object mapping epic_id -> { closed, total }
|
|
155
|
+
EPIC_STATS="{}"
|
|
156
|
+
if [ -n "$EPIC_IDS" ]; then
|
|
157
|
+
# Process each epic (bash 3.2 compatible — read line by line)
|
|
158
|
+
while IFS= read -r epic_id; do
|
|
159
|
+
[ -z "$epic_id" ] && continue
|
|
160
|
+
CHILDREN_JSON="$("$BD" children "$epic_id" --json 2>/dev/null || echo '[]')"
|
|
161
|
+
TOTAL="$(printf '%s' "$CHILDREN_JSON" | jq 'length')"
|
|
162
|
+
CLOSED="$(printf '%s' "$CHILDREN_JSON" | jq '[.[] | select(.status == "closed")] | length')"
|
|
163
|
+
EPIC_STATS="$(printf '%s' "$EPIC_STATS" | jq --arg id "$epic_id" --argjson total "$TOTAL" --argjson closed "$CLOSED" '. + {($id): {total: $total, closed: $closed}}')"
|
|
164
|
+
done <<EOF
|
|
165
|
+
$EPIC_IDS
|
|
166
|
+
EOF
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# ── Score issues with jq ────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
SCORED_JSON="$(printf '%s' "$ISSUES_JSON" | jq --argjson epic_stats "$EPIC_STATS" '
|
|
172
|
+
# First pass: build reverse dependency map (who does each issue unblock?)
|
|
173
|
+
# For each issue X, find all issues Y where Y.dependencies[].depends_on_id == X.id
|
|
174
|
+
(reduce .[] as $dep_issue ({};
|
|
175
|
+
reduce ($dep_issue.dependencies // [] | .[]) as $dep (.;
|
|
176
|
+
.[$dep.depends_on_id] += [$dep_issue.id]
|
|
177
|
+
)
|
|
178
|
+
)) as $dependents_map |
|
|
179
|
+
|
|
180
|
+
[.[] | . as $issue |
|
|
181
|
+
|
|
182
|
+
# Priority weight: P0=5, P1=4, P2=3, P3=2, P4=1, default=1
|
|
183
|
+
# Normalize priority to number (beads 0.62+ sends numbers, older sends strings)
|
|
184
|
+
((.priority // 2) | if type == "string" then
|
|
185
|
+
(if . == "P0" then 0 elif . == "P1" then 1 elif . == "P2" then 2 elif . == "P3" then 3 elif . == "P4" then 4 else 5 end)
|
|
186
|
+
else . end) as $pri_num |
|
|
187
|
+
(if $pri_num == 0 then 5
|
|
188
|
+
elif $pri_num == 1 then 4
|
|
189
|
+
elif $pri_num == 2 then 3
|
|
190
|
+
elif $pri_num == 3 then 2
|
|
191
|
+
elif $pri_num == 4 then 1
|
|
192
|
+
else 1 end) as $priority_weight |
|
|
193
|
+
|
|
194
|
+
# Unblock chain: dependent_count + 1 (min 1).
|
|
195
|
+
# NOTE: dependent_count is the authoritative server-side count. The "Unblocks:"
|
|
196
|
+
# annotation (computed below from local response) may show fewer items when
|
|
197
|
+
# bd list omits closed or filtered-out issues. This is an intentional trade-off.
|
|
198
|
+
(((.dependent_count // 0) + 1) | if . < 1 then 1 else . end) as $unblock_chain |
|
|
199
|
+
|
|
200
|
+
# Type weight: bug=1.2, feature=1.0, task=0.8, default=1.0
|
|
201
|
+
# Handle null type from beads 0.62+
|
|
202
|
+
(if (.type // "task") == "bug" then 1.2
|
|
203
|
+
elif (.type // "task") == "feature" then 1.0
|
|
204
|
+
elif (.type // "task") == "task" then 0.8
|
|
205
|
+
else 1.0 end) as $type_weight |
|
|
206
|
+
|
|
207
|
+
# Status boost: in_progress=1.5, open=1.0, default=1.0
|
|
208
|
+
(if .status == "in_progress" then 1.5
|
|
209
|
+
elif .status == "open" then 1.0
|
|
210
|
+
else 1.0 end) as $status_boost |
|
|
211
|
+
|
|
212
|
+
# Epic proximity: if issue has parent_id that is an epic, compute
|
|
213
|
+
# 1.0 + (closed_siblings / total_siblings) * 0.5
|
|
214
|
+
(if (.parent_id // "") != "" and ($epic_stats[.parent_id] // null) != null then
|
|
215
|
+
$epic_stats[.parent_id] as $es |
|
|
216
|
+
if $es.total > 0 then
|
|
217
|
+
1.0 + (($es.closed / $es.total) * 0.5)
|
|
218
|
+
else 1.0 end
|
|
219
|
+
else 1.0 end) as $epic_proximity |
|
|
220
|
+
|
|
221
|
+
# Staleness boost based on updated_at
|
|
222
|
+
# 0-7d=1.0, 7-14d=1.1, 14-30d=1.2, 30+d=1.5
|
|
223
|
+
# Truncate to first 19 chars (YYYY-MM-DDTHH:MM:SS) + "Z" for fromdateiso8601.
|
|
224
|
+
# Handles fractional seconds (.NNN), timezone offsets (+05:30), or both.
|
|
225
|
+
(if .updated_at then
|
|
226
|
+
((.updated_at[:19] + "Z") | fromdateiso8601) as $ts |
|
|
227
|
+
((now - $ts) / 86400) as $days |
|
|
228
|
+
if $days >= 30 then 1.5
|
|
229
|
+
elif $days >= 14 then 1.2
|
|
230
|
+
elif $days >= 7 then 1.1
|
|
231
|
+
else 1.0 end
|
|
232
|
+
else 1.0 end) as $staleness_boost |
|
|
233
|
+
|
|
234
|
+
# Composite score
|
|
235
|
+
($priority_weight * $unblock_chain * $type_weight * $status_boost * $epic_proximity * $staleness_boost) as $score |
|
|
236
|
+
|
|
237
|
+
# Compute dependents list (which issues does this one unblock?)
|
|
238
|
+
($dependents_map[$issue.id] // []) as $dependents_list |
|
|
239
|
+
|
|
240
|
+
# Output scored issue
|
|
241
|
+
$issue + {
|
|
242
|
+
score: ($score * 100 | round / 100),
|
|
243
|
+
priority_weight: $priority_weight,
|
|
244
|
+
unblock_chain: $unblock_chain,
|
|
245
|
+
type_weight: $type_weight,
|
|
246
|
+
status_boost: $status_boost,
|
|
247
|
+
epic_proximity: (($epic_proximity * 100 | round) / 100),
|
|
248
|
+
staleness_boost: $staleness_boost,
|
|
249
|
+
dependents: $dependents_list
|
|
250
|
+
}
|
|
251
|
+
] | sort_by(-.score)
|
|
252
|
+
')"
|
|
253
|
+
|
|
254
|
+
# ── Session detection ─────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
# Parse git worktree list --porcelain to find active sessions
|
|
257
|
+
# Format: blocks separated by blank lines, each block has:
|
|
258
|
+
# worktree <path>
|
|
259
|
+
# HEAD <sha>
|
|
260
|
+
# branch refs/heads/<branch>
|
|
261
|
+
|
|
262
|
+
WORKTREE_PORCELAIN="$("$GIT" worktree list --porcelain 2>/dev/null || echo '')"
|
|
263
|
+
|
|
264
|
+
# Collect non-default-branch worktree branches and paths
|
|
265
|
+
# Stored as newline-delimited "branch<TAB>path" entries — safe for paths with |, spaces, etc.
|
|
266
|
+
SESSION_ENTRIES=""
|
|
267
|
+
SESSION_COUNT=0
|
|
268
|
+
|
|
269
|
+
_wt_path=""
|
|
270
|
+
_wt_branch=""
|
|
271
|
+
while IFS= read -r line; do
|
|
272
|
+
case "$line" in
|
|
273
|
+
worktree\ *)
|
|
274
|
+
_wt_path="${line#worktree }"
|
|
275
|
+
;;
|
|
276
|
+
branch\ *)
|
|
277
|
+
_wt_branch="${line#branch refs/heads/}"
|
|
278
|
+
;;
|
|
279
|
+
"")
|
|
280
|
+
# End of block — process if we have a branch that is not the default branch
|
|
281
|
+
if [ -n "$_wt_branch" ] && [ "$_wt_branch" != "$BASE_BRANCH" ]; then
|
|
282
|
+
SESSION_COUNT=$((SESSION_COUNT + 1))
|
|
283
|
+
if [ -n "$SESSION_ENTRIES" ]; then
|
|
284
|
+
SESSION_ENTRIES="${SESSION_ENTRIES}
|
|
285
|
+
${_wt_branch} ${_wt_path}"
|
|
286
|
+
else
|
|
287
|
+
SESSION_ENTRIES="${_wt_branch} ${_wt_path}"
|
|
288
|
+
fi
|
|
289
|
+
fi
|
|
290
|
+
_wt_path=""
|
|
291
|
+
_wt_branch=""
|
|
292
|
+
;;
|
|
293
|
+
esac
|
|
294
|
+
done <<EOF
|
|
295
|
+
${WORKTREE_PORCELAIN}
|
|
296
|
+
|
|
297
|
+
EOF
|
|
298
|
+
|
|
299
|
+
# Get in-progress issues for matching (only if we have sessions)
|
|
300
|
+
IN_PROGRESS_JSON="[]"
|
|
301
|
+
if [ "$SESSION_COUNT" -gt 0 ]; then
|
|
302
|
+
IN_PROGRESS_JSON="$("$BD" list --status in_progress --json --limit 0 2>/dev/null || echo '')"
|
|
303
|
+
# Fallback: only if bd command failed (empty output), not if result is legitimately empty array
|
|
304
|
+
if [ -z "$IN_PROGRESS_JSON" ] || ! printf '%s' "$IN_PROGRESS_JSON" | jq empty 2>/dev/null; then
|
|
305
|
+
IN_PROGRESS_JSON="$(printf '%s' "$ISSUES_JSON" | jq '[.[] | select(.status == "in_progress")]')"
|
|
306
|
+
fi
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
# Build sessions data: for each branch, find matching in-progress issues
|
|
310
|
+
# Match by: branch slug (after feat/, fix/, docs/) appears in issue title (case-insensitive, hyphen-to-space)
|
|
311
|
+
SESSIONS_JSON="[]"
|
|
312
|
+
if [ "$SESSION_COUNT" -gt 0 ]; then
|
|
313
|
+
# Process each session (newline-delimited "branch<TAB>path" entries)
|
|
314
|
+
while IFS=' ' read -r _branch _path; do
|
|
315
|
+
[ -z "$_branch" ] && continue
|
|
316
|
+
|
|
317
|
+
# Extract slug from branch name (strip any <prefix>/ convention)
|
|
318
|
+
_slug="$_branch"
|
|
319
|
+
case "$_slug" in
|
|
320
|
+
*/*) _slug="${_slug##*/}" ;;
|
|
321
|
+
esac
|
|
322
|
+
|
|
323
|
+
# Convert slug hyphens to spaces for title matching
|
|
324
|
+
_slug_spaces="$(printf '%s' "$_slug" | tr '-' ' ')"
|
|
325
|
+
|
|
326
|
+
# Find matching in-progress issues: title contains slug words (case-insensitive)
|
|
327
|
+
_matched_ids="$(printf '%s' "$IN_PROGRESS_JSON" | jq -r --arg slug "$_slug_spaces" '
|
|
328
|
+
[.[] | select(
|
|
329
|
+
(.title | ascii_downcase | contains($slug | ascii_downcase))
|
|
330
|
+
) | .id] | join(",")
|
|
331
|
+
')"
|
|
332
|
+
|
|
333
|
+
_issue_count=0
|
|
334
|
+
if [ -n "$_matched_ids" ]; then
|
|
335
|
+
# Count commas + 1
|
|
336
|
+
_issue_count="$(printf '%s' "$_matched_ids" | tr -cd ',' | wc -c)"
|
|
337
|
+
_issue_count=$((_issue_count + 1))
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
# Build session JSON entry
|
|
341
|
+
if [ -n "$_matched_ids" ]; then
|
|
342
|
+
_ids_json="$(printf '%s' "$_matched_ids" | jq -R 'split(",")')"
|
|
343
|
+
else
|
|
344
|
+
_ids_json="[]"
|
|
345
|
+
fi
|
|
346
|
+
|
|
347
|
+
SESSIONS_JSON="$(printf '%s' "$SESSIONS_JSON" | jq \
|
|
348
|
+
--arg branch "$_branch" \
|
|
349
|
+
--arg path "$_path" \
|
|
350
|
+
--argjson ids "$_ids_json" \
|
|
351
|
+
--argjson count "$_issue_count" \
|
|
352
|
+
'. + [{branch: $branch, path: $path, issue_ids: $ids, issue_count: $count}]'
|
|
353
|
+
)"
|
|
354
|
+
|
|
355
|
+
done <<SESSIONS_EOF
|
|
356
|
+
$SESSION_ENTRIES
|
|
357
|
+
SESSIONS_EOF
|
|
358
|
+
fi
|
|
359
|
+
|
|
360
|
+
# ── File-level conflict detection ────────────────────────────────────────
|
|
361
|
+
# For each active worktree branch, get changed files via:
|
|
362
|
+
# git diff <base-branch>...<branch> --name-only --
|
|
363
|
+
# Uses -- separator to prevent argument injection (OWASP A03)
|
|
364
|
+
|
|
365
|
+
# ALL_BRANCH_FILES: newline-delimited list of "branch<TAB>file1<TAB>file2..."
|
|
366
|
+
ALL_BRANCH_FILES=""
|
|
367
|
+
if [ "$SESSION_COUNT" -ge 2 ]; then
|
|
368
|
+
# Read branches from SESSION_ENTRIES (newline-delimited "branch<TAB>path")
|
|
369
|
+
while IFS=' ' read -r _branch _path; do
|
|
370
|
+
[ -z "$_branch" ] && continue
|
|
371
|
+
|
|
372
|
+
# Get changed files for this branch vs BASE_BRANCH (-- prevents injection)
|
|
373
|
+
_files="$("$GIT" diff "${BASE_BRANCH}...${_branch}" --name-only -- 2>/dev/null || echo '')"
|
|
374
|
+
# Collapse to tab-delimited (tabs can't appear in filenames), strip empty lines
|
|
375
|
+
_files_csv="$(printf '%s' "$_files" | tr '\n' '\t' | sed 's/\t$//' | sed 's/^\t//')"
|
|
376
|
+
|
|
377
|
+
if [ -n "$ALL_BRANCH_FILES" ]; then
|
|
378
|
+
ALL_BRANCH_FILES="${ALL_BRANCH_FILES}
|
|
379
|
+
${_branch}:${_files_csv}"
|
|
380
|
+
else
|
|
381
|
+
ALL_BRANCH_FILES="${_branch}:${_files_csv}"
|
|
382
|
+
fi
|
|
383
|
+
done <<CONFLICT_EOF
|
|
384
|
+
$SESSION_ENTRIES
|
|
385
|
+
CONFLICT_EOF
|
|
386
|
+
|
|
387
|
+
# Build conflict map: find files that appear in multiple branches
|
|
388
|
+
# and add changed_files + conflicts to SESSIONS_JSON
|
|
389
|
+
while IFS= read -r _entry; do
|
|
390
|
+
[ -z "$_entry" ] && continue
|
|
391
|
+
_branch="${_entry%%:*}"
|
|
392
|
+
_files_csv="${_entry#*:}"
|
|
393
|
+
|
|
394
|
+
# Convert tab-delimited files to JSON array
|
|
395
|
+
if [ -n "$_files_csv" ]; then
|
|
396
|
+
_files_json="$(printf '%s' "$_files_csv" | jq -R 'split("\t")')"
|
|
397
|
+
else
|
|
398
|
+
_files_json="[]"
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
# Find conflicts: files in this branch that also appear in other branches
|
|
402
|
+
_conflicts_json="[]"
|
|
403
|
+
while IFS= read -r _other_entry; do
|
|
404
|
+
[ -z "$_other_entry" ] && continue
|
|
405
|
+
_other_branch="${_other_entry%%:*}"
|
|
406
|
+
_other_files="${_other_entry#*:}"
|
|
407
|
+
|
|
408
|
+
# Skip self
|
|
409
|
+
[ "$_other_branch" = "$_branch" ] && continue
|
|
410
|
+
[ -z "$_other_files" ] && continue
|
|
411
|
+
|
|
412
|
+
# Find intersection
|
|
413
|
+
_other_json="$(printf '%s' "$_other_files" | jq -R 'split("\t")')"
|
|
414
|
+
_overlap="$(jq -n --argjson a "$_files_json" --argjson b "$_other_json" \
|
|
415
|
+
'[$a[] as $f | select($b | index($f))] | unique')"
|
|
416
|
+
_overlap_len="$(printf '%s' "$_overlap" | jq 'length')"
|
|
417
|
+
if [ "$_overlap_len" -gt 0 ]; then
|
|
418
|
+
_conflicts_json="$(jq -n \
|
|
419
|
+
--argjson existing "$_conflicts_json" \
|
|
420
|
+
--arg other_branch "$_other_branch" \
|
|
421
|
+
--argjson files "$_overlap" \
|
|
422
|
+
'$existing + [{branch: $other_branch, files: $files}]')"
|
|
423
|
+
fi
|
|
424
|
+
done <<INNER_EOF
|
|
425
|
+
$ALL_BRANCH_FILES
|
|
426
|
+
INNER_EOF
|
|
427
|
+
|
|
428
|
+
# Update SESSIONS_JSON: add changed_files and conflicts to matching branch entry
|
|
429
|
+
SESSIONS_JSON="$(printf '%s' "$SESSIONS_JSON" | jq \
|
|
430
|
+
--arg branch "$_branch" \
|
|
431
|
+
--argjson files "$_files_json" \
|
|
432
|
+
--argjson conflicts "$_conflicts_json" \
|
|
433
|
+
'[.[] | if .branch == $branch then . + {changed_files: $files, conflicts: $conflicts} else . end]'
|
|
434
|
+
)"
|
|
435
|
+
done <<OUTER_EOF
|
|
436
|
+
$ALL_BRANCH_FILES
|
|
437
|
+
OUTER_EOF
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
# ── Tier 2: git merge-tree conflict detection ──────────────────────────
|
|
441
|
+
# If git >= 2.38 and there are file overlaps from Tier 1, run merge-tree
|
|
442
|
+
# to detect actual merge conflicts vs mere file overlap.
|
|
443
|
+
|
|
444
|
+
TIER2_ENABLED=0
|
|
445
|
+
if [ "$SESSION_COUNT" -ge 2 ]; then
|
|
446
|
+
# Check git version >= 2.38 (merge-tree --write-tree requires it)
|
|
447
|
+
_git_ver="$("$GIT" --version 2>/dev/null || echo '')"
|
|
448
|
+
_git_major=0
|
|
449
|
+
_git_minor=0
|
|
450
|
+
# Parse "git version X.Y.Z..." -> extract major and minor
|
|
451
|
+
_ver_nums="${_git_ver#git version }"
|
|
452
|
+
_git_major="${_ver_nums%%.*}"
|
|
453
|
+
_ver_rest="${_ver_nums#*.}"
|
|
454
|
+
_git_minor="${_ver_rest%%.*}"
|
|
455
|
+
# Validate they are numbers
|
|
456
|
+
case "$_git_major" in ''|*[!0-9]*) _git_major=0 ;; esac
|
|
457
|
+
case "$_git_minor" in ''|*[!0-9]*) _git_minor=0 ;; esac
|
|
458
|
+
|
|
459
|
+
if [ "$_git_major" -gt 2 ] || { [ "$_git_major" -eq 2 ] && [ "$_git_minor" -ge 38 ]; }; then
|
|
460
|
+
TIER2_ENABLED=1
|
|
461
|
+
fi
|
|
462
|
+
fi
|
|
463
|
+
|
|
464
|
+
# For each pair of branches with file overlaps, run merge-tree
|
|
465
|
+
if [ "$TIER2_ENABLED" = "1" ]; then
|
|
466
|
+
# Collect all branch pairs that have conflicts in SESSIONS_JSON
|
|
467
|
+
_conflict_pairs="$(printf '%s' "$SESSIONS_JSON" | jq -r '
|
|
468
|
+
[.[] | select((.conflicts // []) | length > 0) |
|
|
469
|
+
.branch as $b |
|
|
470
|
+
.conflicts[] |
|
|
471
|
+
[$b, .branch] | sort | join(" ")
|
|
472
|
+
] | unique | .[]
|
|
473
|
+
')"
|
|
474
|
+
|
|
475
|
+
if [ -n "$_conflict_pairs" ]; then
|
|
476
|
+
# Process each unique pair
|
|
477
|
+
while IFS= read -r _pair; do
|
|
478
|
+
[ -z "$_pair" ] && continue
|
|
479
|
+
_b1="${_pair%% *}"
|
|
480
|
+
_b2="${_pair#* }"
|
|
481
|
+
|
|
482
|
+
# Run merge-tree; exit 1 = actual conflict, exit 0 = clean merge
|
|
483
|
+
_mt_output=""
|
|
484
|
+
_mt_exit=0
|
|
485
|
+
_mt_output="$("$GIT" merge-tree --write-tree --name-only --no-messages -- "$_b1" "$_b2" 2>/dev/null)" || _mt_exit=$?
|
|
486
|
+
|
|
487
|
+
if [ "$_mt_exit" -ne 0 ] && [ -n "$_mt_output" ]; then
|
|
488
|
+
# Real conflict detected — parse conflicted file names
|
|
489
|
+
# merge-tree outputs a tree SHA on first line, then filenames
|
|
490
|
+
# With --name-only, conflicted files are listed after the SHA line
|
|
491
|
+
_conflict_files=""
|
|
492
|
+
_line_num=0
|
|
493
|
+
while IFS= read -r _cf_line; do
|
|
494
|
+
_line_num=$((_line_num + 1))
|
|
495
|
+
# Skip the first line (tree SHA output from merge-tree)
|
|
496
|
+
if [ "$_line_num" -eq 1 ]; then continue; fi
|
|
497
|
+
# Skip empty lines
|
|
498
|
+
if [ -z "$_cf_line" ]; then continue; fi
|
|
499
|
+
if [ -n "$_conflict_files" ]; then
|
|
500
|
+
_conflict_files="${_conflict_files} ${_cf_line}"
|
|
501
|
+
else
|
|
502
|
+
_conflict_files="${_cf_line}"
|
|
503
|
+
fi
|
|
504
|
+
done <<MTEOF
|
|
505
|
+
$_mt_output
|
|
506
|
+
MTEOF
|
|
507
|
+
|
|
508
|
+
if [ -n "$_conflict_files" ]; then
|
|
509
|
+
_cf_json="$(printf '%s' "$_conflict_files" | jq -R 'split("\t")')"
|
|
510
|
+
|
|
511
|
+
# Add merge_conflicts to both branches in SESSIONS_JSON
|
|
512
|
+
for _target_branch in "$_b1" "$_b2"; do
|
|
513
|
+
_other_branch="$_b1"
|
|
514
|
+
[ "$_target_branch" = "$_b1" ] && _other_branch="$_b2"
|
|
515
|
+
|
|
516
|
+
SESSIONS_JSON="$(printf '%s' "$SESSIONS_JSON" | jq \
|
|
517
|
+
--arg branch "$_target_branch" \
|
|
518
|
+
--arg other "$_other_branch" \
|
|
519
|
+
--argjson files "$_cf_json" \
|
|
520
|
+
'[.[] | if .branch == $branch then
|
|
521
|
+
. + {merge_conflicts: ((.merge_conflicts // []) + [{branch: $other, files: $files}])}
|
|
522
|
+
else . end]'
|
|
523
|
+
)"
|
|
524
|
+
done
|
|
525
|
+
fi
|
|
526
|
+
fi
|
|
527
|
+
done <<PAIRSEOF
|
|
528
|
+
$_conflict_pairs
|
|
529
|
+
PAIRSEOF
|
|
530
|
+
fi
|
|
531
|
+
fi
|
|
532
|
+
|
|
533
|
+
# ── Cross-developer team activity ────────────────────────────────────────
|
|
534
|
+
# Read file index entries from all developers, identify OTHER developers'
|
|
535
|
+
# in-progress work, compute module overlaps, and flag stale claims.
|
|
536
|
+
|
|
537
|
+
TEAM_ACTIVITY_JSON="[]"
|
|
538
|
+
|
|
539
|
+
# Only proceed if file_index_read and get_session_identity are available
|
|
540
|
+
if command -v file_index_read &>/dev/null && command -v get_session_identity &>/dev/null; then
|
|
541
|
+
_file_index_all="$(file_index_read 2>/dev/null || echo '[]')"
|
|
542
|
+
_file_index_len="$(printf '%s' "$_file_index_all" | jq 'length')"
|
|
543
|
+
|
|
544
|
+
if [ "$_file_index_len" -gt 0 ]; then
|
|
545
|
+
_my_identity="$(get_session_identity 2>/dev/null || echo '')"
|
|
546
|
+
|
|
547
|
+
# Allow override via SMART_STATUS_IDENTITY env (for testing)
|
|
548
|
+
if [ -n "${SMART_STATUS_IDENTITY:-}" ]; then
|
|
549
|
+
_my_identity="$SMART_STATUS_IDENTITY"
|
|
550
|
+
fi
|
|
551
|
+
|
|
552
|
+
if [ -n "$_my_identity" ]; then
|
|
553
|
+
# Build team activity: filter out current dev, compute overlaps and staleness
|
|
554
|
+
# Current dev's modules (for overlap detection)
|
|
555
|
+
_my_modules="$(printf '%s' "$_file_index_all" | jq -c --arg me "$_my_identity" \
|
|
556
|
+
'[.[] | select(.developer == $me) | .modules // [] | .[]] | unique')" || _my_modules="[]"
|
|
557
|
+
|
|
558
|
+
# STALENESS_THRESHOLD_SECS = 48 hours = 172800 seconds
|
|
559
|
+
TEAM_ACTIVITY_JSON="$(printf '%s' "$_file_index_all" | jq -c --arg me "$_my_identity" \
|
|
560
|
+
--argjson my_modules "$_my_modules" \
|
|
561
|
+
--arg now_ts "$(date +%s)" '
|
|
562
|
+
[.[] | select(.developer != $me) |
|
|
563
|
+
. as $entry |
|
|
564
|
+
# Compute staleness
|
|
565
|
+
(if .updated_at then
|
|
566
|
+
((.updated_at | sub("\\.[0-9]+Z$"; "Z")) | fromdateiso8601) as $ts |
|
|
567
|
+
(($now_ts | tonumber) - $ts) as $age_secs |
|
|
568
|
+
{
|
|
569
|
+
age_secs: $age_secs,
|
|
570
|
+
stale: ($age_secs > 172800),
|
|
571
|
+
days_ago: (($age_secs / 86400) | floor)
|
|
572
|
+
}
|
|
573
|
+
else
|
|
574
|
+
{ age_secs: 0, stale: false, days_ago: 0 }
|
|
575
|
+
end) as $staleness |
|
|
576
|
+
# Compute module overlaps
|
|
577
|
+
([$entry.modules // [] | .[] | select(. as $m | $my_modules | index($m))] | unique) as $overlaps |
|
|
578
|
+
{
|
|
579
|
+
developer: $entry.developer,
|
|
580
|
+
issue_id: $entry.issue_id,
|
|
581
|
+
modules: ($entry.modules // []),
|
|
582
|
+
files: ($entry.files // []),
|
|
583
|
+
stale: $staleness.stale,
|
|
584
|
+
days_ago: $staleness.days_ago,
|
|
585
|
+
overlapping_modules: $overlaps,
|
|
586
|
+
updated_at: $entry.updated_at
|
|
587
|
+
}
|
|
588
|
+
] | sort_by(.developer) | group_by(.developer) | map({
|
|
589
|
+
developer: .[0].developer,
|
|
590
|
+
issues: [.[] | {
|
|
591
|
+
issue_id: .issue_id,
|
|
592
|
+
modules: .modules,
|
|
593
|
+
files: .files,
|
|
594
|
+
stale: .stale,
|
|
595
|
+
days_ago: .days_ago,
|
|
596
|
+
overlapping_modules: .overlapping_modules
|
|
597
|
+
}]
|
|
598
|
+
})
|
|
599
|
+
')" || TEAM_ACTIVITY_JSON="[]"
|
|
600
|
+
fi
|
|
601
|
+
fi
|
|
602
|
+
fi
|
|
603
|
+
|
|
604
|
+
# ── Output ──────────────────────────────────────────────────────────────
|
|
605
|
+
|
|
606
|
+
if [ "$JSON_MODE" = "1" ]; then
|
|
607
|
+
# Always output consistent shape: {sessions: [...], issues: [...], team_activity: [...]}
|
|
608
|
+
jq -n --argjson sessions "$SESSIONS_JSON" --argjson issues "$SCORED_JSON" \
|
|
609
|
+
--argjson team_activity "$TEAM_ACTIVITY_JSON" \
|
|
610
|
+
'{sessions: $sessions, issues: $issues, team_activity: $team_activity}'
|
|
611
|
+
else
|
|
612
|
+
# Grouped, colored output
|
|
613
|
+
# NO_COLOR support: https://no-color.org/
|
|
614
|
+
if [ -n "${NO_COLOR:-}" ]; then
|
|
615
|
+
C_RESET="" C_GREEN="" C_YELLOW="" C_RED="" C_DIM="" C_BOLD="" C_CYAN=""
|
|
616
|
+
else
|
|
617
|
+
C_RESET=$'\033[0m' C_GREEN=$'\033[32m' C_YELLOW=$'\033[33m'
|
|
618
|
+
C_RED=$'\033[31m' C_DIM=$'\033[2m' C_BOLD=$'\033[1m' C_CYAN=$'\033[36m'
|
|
619
|
+
fi
|
|
620
|
+
|
|
621
|
+
# ── Active Sessions (before grouped output) ──
|
|
622
|
+
if [ "$SESSION_COUNT" -gt 0 ]; then
|
|
623
|
+
printf '%s%s=== ACTIVE SESSIONS ===%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET"
|
|
624
|
+
printf '%s' "$SESSIONS_JSON" | jq -r '.[] |
|
|
625
|
+
.branch as $branch |
|
|
626
|
+
.issue_ids as $ids |
|
|
627
|
+
.issue_count as $count |
|
|
628
|
+
.changed_files as $files |
|
|
629
|
+
.conflicts as $conflicts |
|
|
630
|
+
.merge_conflicts as $merge_conflicts |
|
|
631
|
+
# Branch line
|
|
632
|
+
(if $count > 0 then
|
|
633
|
+
" " + $branch + " -> " + ($ids | join(", ")) + " (" + ($count | tostring) + " issue" + (if $count > 1 then "s" else "" end) + ")"
|
|
634
|
+
else
|
|
635
|
+
" " + $branch + " (untracked)"
|
|
636
|
+
end),
|
|
637
|
+
# Changed files line (up to 3, then +N more)
|
|
638
|
+
(if ($files // [] | length) > 0 then
|
|
639
|
+
($files | length) as $total |
|
|
640
|
+
(if $total > 3 then
|
|
641
|
+
" Changed: " + ($files[:3] | join(", ")) + " (+" + (($total - 3) | tostring) + " more)"
|
|
642
|
+
else
|
|
643
|
+
" Changed: " + ($files | join(", "))
|
|
644
|
+
end)
|
|
645
|
+
else empty end),
|
|
646
|
+
# Tier 2: Merge conflict annotations (real conflicts from merge-tree)
|
|
647
|
+
(if ($merge_conflicts // [] | length) > 0 then
|
|
648
|
+
($merge_conflicts[] |
|
|
649
|
+
.branch as $other |
|
|
650
|
+
.files[] |
|
|
651
|
+
" !! Merge conflict: " + . + " (" + $branch + " vs " + $other + ")"
|
|
652
|
+
)
|
|
653
|
+
else empty end),
|
|
654
|
+
# Tier 1: Conflict risk annotations (file overlap only, excluding merge conflicts)
|
|
655
|
+
(if ($conflicts // [] | length) > 0 then
|
|
656
|
+
# Build set of files that have real merge conflicts
|
|
657
|
+
([$merge_conflicts // [] | .[] | .files[]] | unique) as $mc_files |
|
|
658
|
+
($conflicts[] |
|
|
659
|
+
.branch as $other |
|
|
660
|
+
.files[] |
|
|
661
|
+
# Only show Conflict risk if NOT already a merge conflict
|
|
662
|
+
if ($mc_files | index(.)) then empty
|
|
663
|
+
else " ! Conflict risk: " + . + " (" + $other + " in-progress)"
|
|
664
|
+
end
|
|
665
|
+
)
|
|
666
|
+
else empty end)
|
|
667
|
+
'
|
|
668
|
+
printf '\n'
|
|
669
|
+
fi
|
|
670
|
+
|
|
671
|
+
# ── Team Activity (cross-developer visibility) ──
|
|
672
|
+
_ta_len="$(printf '%s' "$TEAM_ACTIVITY_JSON" | jq 'length')"
|
|
673
|
+
if [ "$_ta_len" -gt 0 ]; then
|
|
674
|
+
printf '%s%s── Team Activity ──────────────────────────────────────%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET"
|
|
675
|
+
printf '%s' "$TEAM_ACTIVITY_JSON" | jq -r '
|
|
676
|
+
.[] |
|
|
677
|
+
.developer as $dev |
|
|
678
|
+
" " + $dev + ":",
|
|
679
|
+
(.issues[] |
|
|
680
|
+
.issue_id as $iid |
|
|
681
|
+
.modules as $mods |
|
|
682
|
+
.stale as $stale |
|
|
683
|
+
.days_ago as $days |
|
|
684
|
+
.overlapping_modules as $overlaps |
|
|
685
|
+
# Issue line with optional stale annotation
|
|
686
|
+
(if $stale then
|
|
687
|
+
" " + $iid + " (in_progress, stale: claimed " + ($days | tostring) + " days ago)"
|
|
688
|
+
else
|
|
689
|
+
" " + $iid + " (in_progress)" +
|
|
690
|
+
(if ($mods | length) > 0 then
|
|
691
|
+
" — touching " + ($mods | join(", "))
|
|
692
|
+
else "" end)
|
|
693
|
+
end),
|
|
694
|
+
# Overlap or no-overlap line
|
|
695
|
+
(if ($overlaps | length) > 0 then
|
|
696
|
+
" ! Overlap: " + ($overlaps | join(", ")) + " (you are also working here)"
|
|
697
|
+
else
|
|
698
|
+
" No overlaps with your work"
|
|
699
|
+
end)
|
|
700
|
+
)
|
|
701
|
+
'
|
|
702
|
+
printf '\n'
|
|
703
|
+
fi
|
|
704
|
+
|
|
705
|
+
# Use jq to assign groups and format, then bash to colorize
|
|
706
|
+
printf '%s\n' "$SCORED_JSON" | jq -r --arg now "$(date +%s)" '
|
|
707
|
+
# Assign group to each issue
|
|
708
|
+
[.[] | . as $item |
|
|
709
|
+
# Compute days since updated_at
|
|
710
|
+
(if .updated_at then
|
|
711
|
+
((.updated_at[:19] + "Z") | fromdateiso8601) as $ts |
|
|
712
|
+
(($now | tonumber) - $ts) / 86400 | floor
|
|
713
|
+
else 0 end) as $days_ago |
|
|
714
|
+
# Group assignment (priority order: RESUME > UNBLOCK_CHAINS > BLOCKED > BACKLOG > READY_WORK)
|
|
715
|
+
# Note: in_progress issues with active blockers still show in RESUME (you started
|
|
716
|
+
# the work, you need to see it) but get a "BLOCKED" annotation in the output.
|
|
717
|
+
(if .status == "in_progress" then "RESUME"
|
|
718
|
+
elif (.dependent_count // 0) >= 2 then "UNBLOCK_CHAINS"
|
|
719
|
+
elif (.dependency_count // 0) > 0 and .status != "closed" then "BLOCKED"
|
|
720
|
+
elif (.priority == "P4" or .priority == 4) then "BACKLOG"
|
|
721
|
+
else "READY_WORK"
|
|
722
|
+
end) as $group |
|
|
723
|
+
# Flag in_progress issues that have active blockers
|
|
724
|
+
(if .status == "in_progress" and (.dependency_count // 0) > 0 then true else false end) as $blocked_resume |
|
|
725
|
+
$item + { group: $group, days_ago: $days_ago, blocked_resume: $blocked_resume }
|
|
726
|
+
] |
|
|
727
|
+
|
|
728
|
+
# Group order mapping
|
|
729
|
+
def group_order:
|
|
730
|
+
if . == "RESUME" then 0
|
|
731
|
+
elif . == "UNBLOCK_CHAINS" then 1
|
|
732
|
+
elif . == "READY_WORK" then 2
|
|
733
|
+
elif . == "BLOCKED" then 3
|
|
734
|
+
elif . == "BACKLOG" then 4
|
|
735
|
+
else 5 end;
|
|
736
|
+
|
|
737
|
+
# Sort by group order, then by score descending within group
|
|
738
|
+
sort_by([(.group | group_order), -.score]) |
|
|
739
|
+
|
|
740
|
+
# Group and format
|
|
741
|
+
group_by(.group) |
|
|
742
|
+
sort_by(.[0].group | group_order) |
|
|
743
|
+
[.[] |
|
|
744
|
+
# Group header
|
|
745
|
+
(.[0].group | if . == "UNBLOCK_CHAINS" then "UNBLOCK CHAINS"
|
|
746
|
+
elif . == "READY_WORK" then "READY WORK"
|
|
747
|
+
else . end) as $header |
|
|
748
|
+
"GROUP:" + $header,
|
|
749
|
+
(to_entries[] |
|
|
750
|
+
.value as $item |
|
|
751
|
+
(.key + 1 | tostring) as $rank |
|
|
752
|
+
# Staleness flag
|
|
753
|
+
(if $item.days_ago >= 7 then " [stale " + ($item.days_ago | tostring) + "d]" else "" end) as $stale |
|
|
754
|
+
# Status + days
|
|
755
|
+
("[" + ($item.status // "open") + " " + ($item.days_ago | tostring) + "d]") as $status_tag |
|
|
756
|
+
# Unblocks annotation
|
|
757
|
+
(if ($item.dependents // [] | length) > 0 then
|
|
758
|
+
" -> Unblocks: " + ($item.dependents | join(", "))
|
|
759
|
+
else "" end) as $unblocks |
|
|
760
|
+
# Blocked-resume warning
|
|
761
|
+
(if $item.blocked_resume then " !! BLOCKED by dependencies" else "" end) as $blocked_warn |
|
|
762
|
+
"ENTRY:" + $item.group + ":" +
|
|
763
|
+
$rank + ". [" + ($item.score | tostring) + "] " +
|
|
764
|
+
$item.id + " (" + (if ($item.priority | type) == "number" then "P" + ($item.priority | tostring) else ($item.priority // "-" | tostring) end) + " " + ($item.type // "-" | tostring) + ") -- " +
|
|
765
|
+
($item.title // "-") + " " + $status_tag + $stale + $blocked_warn + $unblocks
|
|
766
|
+
),
|
|
767
|
+
""
|
|
768
|
+
] | .[]
|
|
769
|
+
' | while IFS= read -r line; do
|
|
770
|
+
case "$line" in
|
|
771
|
+
GROUP:RESUME)
|
|
772
|
+
printf '%s%s=== RESUME ===%s\n' "$C_BOLD" "$C_GREEN" "$C_RESET"
|
|
773
|
+
;;
|
|
774
|
+
GROUP:UNBLOCK\ CHAINS)
|
|
775
|
+
printf '%s%s=== UNBLOCK CHAINS ===%s\n' "$C_BOLD" "$C_CYAN" "$C_RESET"
|
|
776
|
+
;;
|
|
777
|
+
GROUP:READY\ WORK)
|
|
778
|
+
printf '%s%s=== READY WORK ===%s\n' "$C_BOLD" "$C_YELLOW" "$C_RESET"
|
|
779
|
+
;;
|
|
780
|
+
GROUP:BLOCKED)
|
|
781
|
+
printf '%s%s=== BLOCKED ===%s\n' "$C_BOLD" "$C_RED" "$C_RESET"
|
|
782
|
+
;;
|
|
783
|
+
GROUP:BACKLOG)
|
|
784
|
+
printf '%s%s=== BACKLOG ===%s\n' "$C_BOLD" "$C_DIM" "$C_RESET"
|
|
785
|
+
;;
|
|
786
|
+
ENTRY:RESUME:*)
|
|
787
|
+
printf ' %s%s%s\n' "$C_GREEN" "${line#ENTRY:RESUME:}" "$C_RESET"
|
|
788
|
+
;;
|
|
789
|
+
ENTRY:UNBLOCK_CHAINS:*)
|
|
790
|
+
printf ' %s%s%s\n' "$C_CYAN" "${line#ENTRY:UNBLOCK_CHAINS:}" "$C_RESET"
|
|
791
|
+
;;
|
|
792
|
+
ENTRY:READY_WORK:*)
|
|
793
|
+
printf ' %s%s%s\n' "$C_YELLOW" "${line#ENTRY:READY_WORK:}" "$C_RESET"
|
|
794
|
+
;;
|
|
795
|
+
ENTRY:BLOCKED:*)
|
|
796
|
+
printf ' %s%s%s\n' "$C_RED" "${line#ENTRY:BLOCKED:}" "$C_RESET"
|
|
797
|
+
;;
|
|
798
|
+
ENTRY:BACKLOG:*)
|
|
799
|
+
printf ' %s%s%s\n' "$C_DIM" "${line#ENTRY:BACKLOG:}" "$C_RESET"
|
|
800
|
+
;;
|
|
801
|
+
"")
|
|
802
|
+
# blank line between groups
|
|
803
|
+
;;
|
|
804
|
+
*)
|
|
805
|
+
printf '%s\n' "$line"
|
|
806
|
+
;;
|
|
807
|
+
esac
|
|
808
|
+
done
|
|
809
|
+
fi
|