forge-workflow 0.1.0-beta.3 → 0.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/AGENTS.md +14 -7
  2. package/CHANGELOG.md +43 -1
  3. package/README.md +6 -2
  4. package/bin/forge-cmd.js +21 -1
  5. package/bin/forge.js +16 -369
  6. package/docs/INDEX.md +1 -1
  7. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  8. package/docs/guides/MIGRATION.md +4 -4
  9. package/docs/guides/SETUP.md +16 -16
  10. package/docs/reference/COMMANDS.md +9 -4
  11. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  12. package/docs/reference/RELEASE.md +5 -3
  13. package/docs/reference/TOOLCHAIN.md +8 -0
  14. package/docs/reference/protected-state-surfaces.md +4 -4
  15. package/docs/reference/shepherd.md +117 -17
  16. package/lefthook.yml +12 -0
  17. package/lib/activation/ensure-forge-home.js +33 -15
  18. package/lib/adapters/greptile-review-adapter.js +1 -1
  19. package/lib/adapters/pr-state-adapter.js +397 -100
  20. package/lib/agents-config.js +5 -0
  21. package/lib/audit-evidence.js +71 -110
  22. package/lib/capped-jsonl-log.js +236 -0
  23. package/lib/commands/_issue.js +31 -46
  24. package/lib/commands/_manifest.js +1 -1
  25. package/lib/commands/_registry.js +2 -2
  26. package/lib/commands/_resolve-command-opts.js +36 -29
  27. package/lib/commands/claim.js +2 -4
  28. package/lib/commands/clean.js +196 -32
  29. package/lib/commands/dev.js +4 -33
  30. package/lib/commands/hooks.js +358 -13
  31. package/lib/commands/insights.js +8 -3
  32. package/lib/commands/merge.js +600 -40
  33. package/lib/commands/plan.js +23 -115
  34. package/lib/commands/pr.js +1 -1
  35. package/lib/commands/preflight.js +11 -2
  36. package/lib/commands/prime.js +23 -3
  37. package/lib/commands/push.js +41 -51
  38. package/lib/commands/recall.js +60 -16
  39. package/lib/commands/recap.js +6 -1
  40. package/lib/commands/release.js +18 -4
  41. package/lib/commands/serve.js +5 -2
  42. package/lib/commands/setup.js +191 -95
  43. package/lib/commands/shepherd.js +49 -4
  44. package/lib/commands/ship.js +22 -23
  45. package/lib/commands/skill.js +383 -0
  46. package/lib/commands/status.js +54 -33
  47. package/lib/commands/test.js +56 -34
  48. package/lib/commands/worktree.js +247 -43
  49. package/lib/core/runtime-graph.js +89 -15
  50. package/lib/doc-assertions.js +297 -0
  51. package/lib/existing-tdd-gate.js +253 -0
  52. package/lib/forge-context.js +1 -4
  53. package/lib/forge-issues.js +64 -491
  54. package/lib/git-defaults.js +56 -0
  55. package/lib/harness-capability-matrix.js +5 -5
  56. package/lib/hook-renderer.js +147 -16
  57. package/lib/insights.js +96 -80
  58. package/lib/issue-backend.js +42 -3
  59. package/lib/kernel/backing-issue.js +14 -2
  60. package/lib/kernel/broker.js +44 -0
  61. package/lib/kernel/cli-broker-factory.js +12 -1
  62. package/lib/kernel/close-on-merge.js +154 -0
  63. package/lib/kernel/fs-class.js +42 -25
  64. package/lib/kernel/migrations.js +30 -2
  65. package/lib/kernel/schema.js +35 -0
  66. package/lib/kernel/sqlite-driver.js +292 -18
  67. package/lib/lefthook-wiring.js +21 -1
  68. package/lib/memory/router.js +16 -1
  69. package/lib/memory-digest.js +47 -15
  70. package/lib/memory-recall-events.js +145 -0
  71. package/lib/memory-recall.js +212 -0
  72. package/lib/merge-rules.js +8 -4
  73. package/lib/npm-publish-workflow.js +272 -0
  74. package/lib/orientation.js +371 -49
  75. package/lib/plugin-catalog.js +14 -4
  76. package/lib/pr-bundle.js +9 -6
  77. package/lib/pr-monitor/journal.js +18 -2
  78. package/lib/pr-monitor/reconcile-executor.js +842 -0
  79. package/lib/pr-monitor/reconcile-tick.js +138 -0
  80. package/lib/pr-monitor/reconcile.js +0 -0
  81. package/lib/pr-monitor/render-summary.js +196 -0
  82. package/lib/pr-monitor/shepherd-lease.js +252 -0
  83. package/lib/pr-monitor/watch-lifecycle.js +14 -2
  84. package/lib/pr-pull.js +98 -24
  85. package/lib/pr-shepherd.js +34 -8
  86. package/lib/preflight/gates.js +65 -18
  87. package/lib/preflight/runner.js +5 -0
  88. package/lib/project-memory.js +40 -0
  89. package/lib/protected-state-authority.js +305 -0
  90. package/lib/protected-state-surfaces.js +64 -44
  91. package/lib/release-readiness.js +51 -4
  92. package/lib/rules-sync.js +4 -0
  93. package/lib/runtime-health.js +15 -46
  94. package/lib/shell-utils.js +1 -1
  95. package/lib/skill-eval.js +750 -0
  96. package/lib/skills-sync.js +6 -3
  97. package/lib/smart-merge.js +28 -4
  98. package/lib/status/identity.js +46 -0
  99. package/lib/status/presenter.js +0 -35
  100. package/lib/status/snapshot.js +11 -16
  101. package/lib/symlink-utils.js +74 -26
  102. package/lib/upgrade-safety.js +47 -9
  103. package/lib/using-forge.js +328 -0
  104. package/lib/workflow/enforce-stage.js +5 -5
  105. package/lib/workflow/state-manager.js +23 -23
  106. package/package.json +6 -7
  107. package/rules/using-forge.md +24 -0
  108. package/scripts/doc-asserting-tests.js +158 -0
  109. package/scripts/forge-team/index.sh +0 -5
  110. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  111. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  112. package/scripts/lib/behavioral-eval-runner.js +310 -0
  113. package/scripts/lib/behavioral-eval-runtime.js +456 -0
  114. package/scripts/lib/eval-evidence.js +328 -0
  115. package/scripts/lib/eval-runner.js +81 -41
  116. package/scripts/lib/immutable-eval-corpus.js +309 -0
  117. package/scripts/lib/promotion-evidence-loader.js +94 -0
  118. package/scripts/lib/promotion-scorecard.js +314 -0
  119. package/scripts/npm-release-receipt.js +134 -0
  120. package/scripts/process-tree.js +761 -0
  121. package/scripts/protected-state-check.js +47 -22
  122. package/scripts/run-command-eval.js +29 -1
  123. package/scripts/sync-d20-audit.js +172 -0
  124. package/scripts/test-full-suite.js +249 -37
  125. package/scripts/test.js +184 -44
  126. package/skills/claim-safety/SKILL.md +4 -0
  127. package/skills/claim-safety/evals/scorecard.json +41 -0
  128. package/skills/coverage.json +83 -0
  129. package/skills/dev/SKILL.md +4 -0
  130. package/skills/dev/evals/scorecard.json +41 -0
  131. package/skills/gates/SKILL.md +80 -0
  132. package/skills/gates/evals/evals.json +38 -0
  133. package/skills/gates/evals/scorecard.json +41 -0
  134. package/skills/hermes-forge/SKILL.md +1 -0
  135. package/skills/hermes-forge/evals/scorecard.json +41 -0
  136. package/skills/issue-basics/SKILL.md +1 -0
  137. package/skills/issue-basics/evals/scorecard.json +41 -0
  138. package/skills/kernel/SKILL.md +38 -0
  139. package/skills/kernel/evals/scorecard.json +41 -0
  140. package/skills/memory/SKILL.md +16 -1
  141. package/skills/memory/evals/scorecard.json +41 -0
  142. package/skills/parallel-deep-research/SKILL.md +1 -0
  143. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  144. package/skills/plan/SKILL.md +6 -0
  145. package/skills/plan/evals/scorecard.json +41 -0
  146. package/skills/portability/SKILL.md +47 -0
  147. package/skills/portability/evals/evals.json +34 -0
  148. package/skills/portability/evals/scorecard.json +41 -0
  149. package/skills/research/SKILL.md +1 -0
  150. package/skills/research/evals/scorecard.json +41 -0
  151. package/skills/review/SKILL.md +10 -11
  152. package/skills/review/evals/scorecard.json +41 -0
  153. package/skills/rollback/SKILL.md +5 -11
  154. package/skills/rollback/evals/scorecard.json +41 -0
  155. package/skills/setup/SKILL.md +91 -0
  156. package/skills/setup/evals/evals.json +42 -0
  157. package/skills/setup/evals/scorecard.json +41 -0
  158. package/skills/shepherd/SKILL.md +84 -38
  159. package/skills/shepherd/evals/evals.json +21 -9
  160. package/skills/shepherd/evals/scorecard.json +41 -0
  161. package/skills/ship/SKILL.md +10 -12
  162. package/skills/ship/evals/scorecard.json +41 -0
  163. package/skills/smith/SKILL.md +8 -0
  164. package/skills/smith/evals/scorecard.json +41 -0
  165. package/skills/sonarcloud/SKILL.md +1 -0
  166. package/skills/sonarcloud/evals/scorecard.json +41 -0
  167. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  168. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  169. package/skills/status/SKILL.md +3 -0
  170. package/skills/status/evals/scorecard.json +41 -0
  171. package/skills/triage-ready/SKILL.md +2 -0
  172. package/skills/triage-ready/evals/scorecard.json +41 -0
  173. package/skills/using-forge/SKILL.md +104 -0
  174. package/skills/using-forge/evals/scorecard.json +41 -0
  175. package/skills/validate/SKILL.md +4 -0
  176. package/skills/validate/evals/scorecard.json +41 -0
  177. package/skills/verify/SKILL.md +4 -0
  178. package/skills/verify/evals/scorecard.json +41 -0
  179. package/skills/worktree/SKILL.md +92 -0
  180. package/skills/worktree/evals/evals.json +38 -0
  181. package/skills/worktree/evals/scorecard.json +41 -0
  182. package/lib/adapters/beads-issue-adapter.js +0 -127
  183. package/lib/beads-nudge.js +0 -91
  184. package/lib/beads-setup.js +0 -538
  185. package/lib/beads-sync-scaffold.js +0 -189
  186. package/lib/commands/board.js +0 -64
  187. package/lib/pat-setup.js +0 -207
  188. package/lib/pr-monitor/render-sticky.js +0 -192
  189. package/lib/pr-monitor/upsert-sticky.js +0 -169
  190. package/lib/status/beads-snapshot.js +0 -145
  191. package/scripts/beads-context.sh +0 -577
  192. package/scripts/beads-migrate-to-dolt.sh +0 -7
  193. package/scripts/beads-upgrade-smoke.sh +0 -284
  194. package/scripts/forge-team/lib/dashboard.sh +0 -316
  195. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -1,284 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- _BEADS_UPGRADE_SMOKE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
- if [[ -f "$_BEADS_UPGRADE_SMOKE_DIR/bootstrap-windows-tools.sh" ]]; then
6
- source "$_BEADS_UPGRADE_SMOKE_DIR/bootstrap-windows-tools.sh"
7
- fi
8
-
9
- BD_CMD="${BD_CMD:-bd}"
10
- NODE_CMD="${NODE_CMD:-node}"
11
- PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}"
12
- ARTIFACT_DIR="${BEADS_UPGRADE_SMOKE_ARTIFACT_DIR:-${PROJECT_ROOT}/.artifacts/beads-upgrade-smoke}"
13
- COMMAND_LOG_PATH="${ARTIFACT_DIR}/commands.jsonl"
14
- SUMMARY_PATH="${ARTIFACT_DIR}/summary.json"
15
- CLEANUP_REASON="Beads upgrade smoke cleanup"
16
- SMOKE_RUN_ID="${BEADS_UPGRADE_SMOKE_RUN_ID:-$(date +%s)-$$}"
17
- PRIMARY_ISSUE_TITLE="Beads upgrade smoke primary (${SMOKE_RUN_ID})"
18
- DEPENDENT_ISSUE_TITLE="Beads upgrade smoke dependent (${SMOKE_RUN_ID})"
19
-
20
- node_fs_path() {
21
- case "$1" in
22
- /[A-Za-z]/*)
23
- _drive="$(printf '%s' "${1:1:1}" | tr '[:lower:]' '[:upper:]')"
24
- printf '%s:/%s\n' "$_drive" "${1:3}"
25
- ;;
26
- *)
27
- if command -v cygpath >/dev/null 2>&1; then
28
- cygpath -m "$1"
29
- else
30
- printf '%s\n' "$1"
31
- fi
32
- ;;
33
- esac
34
- }
35
-
36
- mkdir -p "${ARTIFACT_DIR}"
37
- : > "${COMMAND_LOG_PATH}"
38
-
39
- created_issue_ids=()
40
- closed_issue_ids=()
41
- failed_step=""
42
- failure_message=""
43
-
44
- parse_issue_id() {
45
- local output="$1"
46
- if [[ "${output}" =~ Created[[:space:]]+issue:[[:space:]]*([[:alnum:]-]+) ]]; then
47
- printf '%s' "${BASH_REMATCH[1]}"
48
- return 0
49
- fi
50
- if [[ "${output}" =~ \"id\"[[:space:]]*:[[:space:]]*\"([[:alnum:]-]+)\" ]]; then
51
- printf '%s' "${BASH_REMATCH[1]}"
52
- return 0
53
- fi
54
- return 1
55
- }
56
-
57
- find_issue_id_by_title() {
58
- local issue_title="$1"
59
- local list_output
60
- list_output="$("${BD_CMD}" list --json --limit=50 2>/dev/null)" || return 1
61
-
62
- "${NODE_CMD}" -e '
63
- const fs = require("node:fs");
64
- const [issueTitle] = process.argv.slice(1);
65
- try {
66
- const rows = JSON.parse(fs.readFileSync(0, "utf8"));
67
- if (!Array.isArray(rows)) {
68
- process.exit(1);
69
- }
70
- const match = rows.find((row) => row && row.title === issueTitle && typeof row.id === "string");
71
- if (!match) {
72
- process.exit(1);
73
- }
74
- process.stdout.write(match.id);
75
- } catch {
76
- process.exit(1);
77
- }
78
- ' "${issue_title}" <<< "${list_output}"
79
- }
80
-
81
- append_command() {
82
- local step="$1"
83
- local command_name="$2"
84
- local command_text="$3"
85
- local exit_code="$4"
86
- local stdout_path="$5"
87
- local stderr_path="$6"
88
-
89
- "${NODE_CMD}" -e '
90
- const fs = require("node:fs");
91
- const [logPath, step, commandName, commandText, exitCode, stdoutPath, stderrPath] = process.argv.slice(1);
92
- const entry = {
93
- step,
94
- commandName,
95
- commandText,
96
- exitCode: Number(exitCode),
97
- stdoutPath,
98
- stderrPath,
99
- };
100
- fs.appendFileSync(logPath, JSON.stringify(entry) + "\n");
101
- ' "$(node_fs_path "${COMMAND_LOG_PATH}")" "${step}" "${command_name}" "${command_text}" "${exit_code}" "$(node_fs_path "${stdout_path}")" "$(node_fs_path "${stderr_path}")"
102
- }
103
-
104
- read_output_snippet() {
105
- local file_path="$1"
106
- if [[ ! -f "${file_path}" ]]; then
107
- return 0
108
- fi
109
-
110
- "${NODE_CMD}" -e '
111
- const fs = require("node:fs");
112
- const [filePath] = process.argv.slice(1);
113
- const text = fs.readFileSync(filePath, "utf8");
114
- process.stdout.write(text.slice(0, 4096));
115
- ' "$(node_fs_path "${file_path}")"
116
- }
117
-
118
- write_summary() {
119
- local ok="$1"
120
-
121
- "${NODE_CMD}" -e '
122
- const fs = require("node:fs");
123
- const [
124
- commandLogPath,
125
- summaryPath,
126
- okValue,
127
- failedStep,
128
- failureMessage,
129
- artifactDir,
130
- createdJson,
131
- closedJson,
132
- ] = process.argv.slice(1);
133
- const commands = fs.readFileSync(commandLogPath, "utf8")
134
- .split(/\r?\n/)
135
- .filter(Boolean)
136
- .map((line) => JSON.parse(line));
137
- const summary = {
138
- ok: okValue === "true",
139
- failedStep: failedStep || null,
140
- failureMessage: failureMessage || null,
141
- commands,
142
- cleanup: {
143
- createdIssueIds: JSON.parse(createdJson),
144
- closedIssueIds: JSON.parse(closedJson),
145
- },
146
- failureArtifact: summaryPath,
147
- artifactDir,
148
- };
149
- fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2) + "\n");
150
- ' "$(node_fs_path "${COMMAND_LOG_PATH}")" "$(node_fs_path "${SUMMARY_PATH}")" "${ok}" "${failed_step}" "${failure_message}" "$(node_fs_path "${ARTIFACT_DIR}")" "$(printf '%s\n' "${created_issue_ids[@]:-}" | "${NODE_CMD}" -e 'const fs=require("node:fs"); const lines=fs.readFileSync(0,"utf8").split(/\r?\n/).filter(Boolean); process.stdout.write(JSON.stringify(lines));')" "$(printf '%s\n' "${closed_issue_ids[@]:-}" | "${NODE_CMD}" -e 'const fs=require("node:fs"); const lines=fs.readFileSync(0,"utf8").split(/\r?\n/).filter(Boolean); process.stdout.write(JSON.stringify(lines));')"
151
- }
152
-
153
- run_bd_step() {
154
- local step="$1"
155
- local command_name="$2"
156
- shift 2
157
-
158
- local stdout_file stderr_file exit_code command_text
159
- stdout_file="${ARTIFACT_DIR}/$(printf '%s' "${step}" | tr '[:upper:]' '[:lower:]').stdout"
160
- stderr_file="${ARTIFACT_DIR}/$(printf '%s' "${step}" | tr '[:upper:]' '[:lower:]').stderr"
161
- command_text="$*"
162
-
163
- if "${BD_CMD}" "$@" >"${stdout_file}" 2>"${stderr_file}"; then
164
- exit_code=0
165
- else
166
- exit_code=$?
167
- fi
168
-
169
- append_command "${step}" "${command_name}" "${command_text}" "${exit_code}" "${stdout_file}" "${stderr_file}"
170
-
171
- if [[ "${exit_code}" -ne 0 ]]; then
172
- failed_step="${command_name}"
173
- local stderr_text stdout_text
174
- stderr_text="$(read_output_snippet "${stderr_file}")"
175
- stdout_text="$(read_output_snippet "${stdout_file}")"
176
- failure_message="${stderr_text:-${stdout_text:-${command_text}}}"
177
- return "${exit_code}"
178
- fi
179
-
180
- cat "${stdout_file}"
181
- }
182
-
183
- is_closed_issue() {
184
- local issue_id="$1"
185
- local existing
186
- for existing in "${closed_issue_ids[@]:-}"; do
187
- if [[ "${existing}" == "${issue_id}" ]]; then
188
- return 0
189
- fi
190
- done
191
- return 1
192
- }
193
-
194
- rollback_unclosed_issues() {
195
- local idx issue_id
196
- for (( idx=${#created_issue_ids[@]} - 1; idx>=0; idx-- )); do
197
- issue_id="${created_issue_ids[idx]}"
198
- if is_closed_issue "${issue_id}"; then
199
- continue
200
- fi
201
- if run_bd_step "rollback-close-${issue_id}" "close" close "${issue_id}" "--reason=${CLEANUP_REASON}" >/dev/null; then
202
- closed_issue_ids+=("${issue_id}")
203
- fi
204
- done
205
- }
206
-
207
- handle_issue_id_parse_failure() {
208
- local issue_title="$1"
209
- local message="$2"
210
- local recovered_issue_id=""
211
-
212
- failed_step="create"
213
- failure_message="${message}"
214
-
215
- recovered_issue_id="$(find_issue_id_by_title "${issue_title}" || true)"
216
- if [[ -n "${recovered_issue_id}" ]]; then
217
- created_issue_ids+=("${recovered_issue_id}")
218
- fi
219
-
220
- rollback_unclosed_issues
221
- write_summary "false"
222
- exit 1
223
- }
224
-
225
- primary_output="$(run_bd_step "create-primary" "create" create --title="${PRIMARY_ISSUE_TITLE}" --type=task --priority=4)" || {
226
- rollback_unclosed_issues
227
- write_summary "false"
228
- exit 1
229
- }
230
- primary_issue_id="$(parse_issue_id "${primary_output}")" || {
231
- handle_issue_id_parse_failure "${PRIMARY_ISSUE_TITLE}" "Could not parse primary smoke issue ID"
232
- }
233
- created_issue_ids+=("${primary_issue_id}")
234
-
235
- dependent_output="$(run_bd_step "create-dependent" "create" create --title="${DEPENDENT_ISSUE_TITLE}" --type=task --priority=4)" || {
236
- rollback_unclosed_issues
237
- write_summary "false"
238
- exit 1
239
- }
240
- dependent_issue_id="$(parse_issue_id "${dependent_output}")" || {
241
- handle_issue_id_parse_failure "${DEPENDENT_ISSUE_TITLE}" "Could not parse dependent smoke issue ID"
242
- }
243
- created_issue_ids+=("${dependent_issue_id}")
244
-
245
- run_bd_step "list" "list" list --json --limit=50 >/dev/null || {
246
- rollback_unclosed_issues
247
- write_summary "false"
248
- exit 1
249
- }
250
-
251
- run_bd_step "show-primary" "show" show "${primary_issue_id}" --json >/dev/null || {
252
- rollback_unclosed_issues
253
- write_summary "false"
254
- exit 1
255
- }
256
-
257
- run_bd_step "dep-add" "dep" dep add "${primary_issue_id}" "${dependent_issue_id}" >/dev/null || {
258
- rollback_unclosed_issues
259
- write_summary "false"
260
- exit 1
261
- }
262
-
263
- run_bd_step "close-dependent" "close" close "${dependent_issue_id}" "--reason=${CLEANUP_REASON}" >/dev/null || {
264
- rollback_unclosed_issues
265
- write_summary "false"
266
- exit 1
267
- }
268
- closed_issue_ids+=("${dependent_issue_id}")
269
-
270
- run_bd_step "close-primary" "close" close "${primary_issue_id}" "--reason=${CLEANUP_REASON}" >/dev/null || {
271
- rollback_unclosed_issues
272
- write_summary "false"
273
- exit 1
274
- }
275
- closed_issue_ids+=("${primary_issue_id}")
276
-
277
- run_bd_step "sync" "sync" sync >/dev/null || {
278
- rollback_unclosed_issues
279
- write_summary "false"
280
- exit 1
281
- }
282
-
283
- write_summary "true"
284
- printf '%s\n' "${SUMMARY_PATH}"
@@ -1,316 +0,0 @@
1
- #!/usr/bin/env bash
2
- # dashboard.sh — Team health dashboard for forge-team.
3
- #
4
- # Provides cmd_dashboard [--format=json] which aggregates team issue data
5
- # from the Forge issue backend and displays per-developer stats, stale
6
- # assignments, and blocked issues.
7
-
8
- # ── Dependencies ─────────────────────────────────────────────────────────
9
- # FORGE_CMD — path to forge binary (default: forge)
10
- FORGE_CMD="${FORGE_CMD:-forge}"
11
-
12
- # FORGE_NOW — override current epoch for testing (default: real time)
13
- _now() {
14
- if [[ -n "${FORGE_NOW:-}" ]]; then
15
- echo "$FORGE_NOW"
16
- else
17
- date +%s
18
- fi
19
- }
20
-
21
- # Convert ISO8601 timestamp to epoch seconds
22
- _iso_to_epoch() {
23
- local ts="$1"
24
- # Remove trailing Z and replace T with space for date parsing
25
- local clean="${ts%Z}"
26
- clean="${clean//T/ }"
27
- # Use date -d for GNU date (Linux/Git Bash)
28
- if date -d "$clean UTC" +%s 2>/dev/null; then
29
- return
30
- fi
31
- # Fallback: manual parse YYYY-MM-DD HH:MM:SS
32
- # This handles the common case in Git Bash on Windows
33
- local year month day hour min sec
34
- IFS='-: ' read -r year month day hour min sec <<< "$clean"
35
- # Use python if available
36
- if command -v python3 &>/dev/null; then
37
- python3 -c "import calendar,datetime; print(int(calendar.timegm(datetime.datetime($year,$month,$day,$hour,$min,$sec).timetuple())))"
38
- elif command -v python &>/dev/null; then
39
- python -c "import calendar,datetime; print(int(calendar.timegm(datetime.datetime($year,$month,$day,$hour,$min,$sec).timetuple())))"
40
- else
41
- # Last resort: approximate with GNU date
42
- echo 0
43
- fi
44
- }
45
-
46
- # ── Data Collection ──────────────────────────────────────────────────────
47
-
48
- # _forge_active_issues_json — Merged JSON array of open + in_progress issues.
49
- # The Kernel list filter does not accept comma-joined statuses, so query each
50
- # status separately and concatenate the issue arrays.
51
- _forge_active_issues_json() {
52
- local open_json inprog_json
53
- open_json="$("$FORGE_CMD" issue list --status=open --json 2>/dev/null)" || open_json=""
54
- inprog_json="$("$FORGE_CMD" issue list --status=in_progress --json 2>/dev/null)" || inprog_json=""
55
- jq -n \
56
- --argjson a "${open_json:-null}" \
57
- --argjson b "${inprog_json:-null}" \
58
- '(((($a.data.issues) // []) + (($b.data.issues) // [])))' 2>/dev/null || echo "[]"
59
- }
60
-
61
- # _issue_detail_lines <issues-json>
62
- # Emit one tab-delimited (jq @tsv) line per issue:
63
- # id<TAB>title<TAB>STATUS<TAB>owner<TAB>updated_iso<TAB>blocked_by
64
- # @tsv escapes any tab/newline/CR inside fields (issue titles are user-controlled
65
- # and may contain '|', tabs, or newlines), so parsing stays field-aligned.
66
- # STATUS is the upper-cased Kernel status; owner is assignee/claimed_by.
67
- # A Kernel issue's `.dependencies` lists the issues it BLOCKS (outgoing edges),
68
- # so blocked_by is computed by reverse scan: the issues (within this active set)
69
- # whose `.dependencies` include this issue.
70
- _issue_detail_lines() {
71
- printf '%s' "$1" | jq -r '
72
- . as $all
73
- | $all[]
74
- | . as $x
75
- | [ $x.id,
76
- ($x.title // ""),
77
- (($x.status // "open") | ascii_upcase),
78
- ($x.assignee // $x.claimed_by // ""),
79
- (($x.updated_at // "") | sub("\\.[0-9]+Z$"; "Z")),
80
- ( [ $all[]
81
- | select( (.dependencies // [])
82
- | map(if type == "object" then (.id // .to // "") else . end)
83
- | any(. == $x.id) )
84
- | .id ]
85
- | join(",") )
86
- ]
87
- | @tsv' | tr -d '\r'
88
- }
89
-
90
- # ── Dashboard Command ────────────────────────────────────────────────────
91
-
92
- cmd_dashboard() {
93
- local format="text"
94
-
95
- # Parse args
96
- local arg
97
- for arg in "$@"; do
98
- case "$arg" in
99
- --format=json) format="json" ;;
100
- --format=*) echo "Error: unsupported format '${arg#--format=}'" >&2; return 1 ;;
101
- esac
102
- done
103
-
104
- # 1. Get all open/in_progress issues as a merged JSON array
105
- local issues_json
106
- issues_json="$(_forge_active_issues_json)"
107
-
108
- # Check for empty
109
- local active_count
110
- active_count="$(printf '%s' "$issues_json" | jq 'length' 2>/dev/null || echo 0)"
111
- if [[ "$active_count" -eq 0 ]]; then
112
- if [[ "$format" == "json" ]]; then
113
- echo '{"developers":{},"total":0,"stale":[],"blocked":[],"message":"No active issues — team is clear"}'
114
- else
115
- echo "No active issues — team is clear"
116
- fi
117
- return 0
118
- fi
119
-
120
- # 2. Build per-issue detail lines: id|title|STATUS|owner|updated_iso|blocked_by
121
- local issues=()
122
- while IFS= read -r entry; do
123
- [[ -z "$entry" ]] && continue
124
- issues+=("$entry")
125
- done <<< "$(_issue_detail_lines "$issues_json")"
126
-
127
- # 3. Collect details for each issue
128
- local now_epoch
129
- now_epoch="$(_now)"
130
- local stale_threshold=172800 # 48h in seconds
131
-
132
- # Associative arrays for per-developer aggregation
133
- declare -A dev_open dev_inprogress dev_blocked dev_stale
134
-
135
- # Lists for stale and blocked sections
136
- local stale_entries=()
137
- local blocked_entries=()
138
- local total_open=0 total_inprogress=0 total_blocked=0 total_stale=0
139
-
140
- for entry in "${issues[@]}"; do
141
- IFS=$'\t' read -r _id _title _status _owner _updated_iso _blocked_by <<< "$entry"
142
-
143
- local _updated_epoch=0
144
- if [[ -n "$_updated_iso" ]]; then
145
- _updated_epoch="$(_iso_to_epoch "$_updated_iso")"
146
- fi
147
-
148
- # Default owner if empty
149
- [[ -z "$_owner" ]] && _owner="unassigned"
150
-
151
- # Initialize developer counters
152
- [[ -z "${dev_open[$_owner]:-}" ]] && dev_open[$_owner]=0
153
- [[ -z "${dev_inprogress[$_owner]:-}" ]] && dev_inprogress[$_owner]=0
154
- [[ -z "${dev_blocked[$_owner]:-}" ]] && dev_blocked[$_owner]=0
155
- [[ -z "${dev_stale[$_owner]:-}" ]] && dev_stale[$_owner]=0
156
-
157
- # Count by status
158
- case "$_status" in
159
- OPEN)
160
- dev_open[$_owner]=$(( ${dev_open[$_owner]} + 1 ))
161
- total_open=$((total_open + 1))
162
- ;;
163
- IN_PROGRESS)
164
- dev_inprogress[$_owner]=$(( ${dev_inprogress[$_owner]} + 1 ))
165
- total_inprogress=$((total_inprogress + 1))
166
- ;;
167
- BLOCKED)
168
- dev_blocked[$_owner]=$(( ${dev_blocked[$_owner]} + 1 ))
169
- total_blocked=$((total_blocked + 1))
170
- ;;
171
- esac
172
-
173
- # Check for blocked (has DEPENDS ON with open deps)
174
- if [[ -n "$_blocked_by" ]]; then
175
- dev_blocked[$_owner]=$(( ${dev_blocked[$_owner]} + 1 ))
176
- total_blocked=$((total_blocked + 1))
177
- blocked_entries+=("$_id"$'\t'"$_owner"$'\t'"$_title"$'\t'"$_blocked_by")
178
- fi
179
-
180
- # Check for stale (>48h since last update)
181
- if [[ "$_updated_epoch" -gt 0 ]]; then
182
- local age=$(( now_epoch - _updated_epoch ))
183
- if [[ $age -gt $stale_threshold ]]; then
184
- local hours=$(( age / 3600 ))
185
- dev_stale[$_owner]=$(( ${dev_stale[$_owner]} + 1 ))
186
- total_stale=$((total_stale + 1))
187
- stale_entries+=("$_id"$'\t'"$_owner"$'\t'"$_title"$'\t'"${hours}h since last update")
188
- fi
189
- fi
190
- done
191
-
192
- local total_issues=${#issues[@]}
193
-
194
- # 4. Output
195
- if [[ "$format" == "json" ]]; then
196
- _dashboard_json "$total_issues"
197
- else
198
- _dashboard_text "$total_issues"
199
- fi
200
- }
201
-
202
- # ── Text Output ──────────────────────────────────────────────────────────
203
-
204
- _dashboard_text() {
205
- local total_issues="$1"
206
-
207
- echo "Team Dashboard"
208
- echo "══════════════"
209
- echo ""
210
-
211
- # Header
212
- printf "%-16s %4s %11s %7s %5s\n" "Developer" "Open" "In Progress" "Blocked" "Stale"
213
-
214
- # Get sorted list of developers
215
- local devs=()
216
- local dev
217
- for dev in "${!dev_open[@]}"; do
218
- devs+=("$dev")
219
- done
220
- # Sort developers
221
- IFS=$'\n' devs=($(sort <<< "${devs[*]}")); unset IFS
222
-
223
- for dev in "${devs[@]}"; do
224
- printf "%-16s %4d %11d %7d %5d\n" \
225
- "$dev" \
226
- "${dev_open[$dev]:-0}" \
227
- "${dev_inprogress[$dev]:-0}" \
228
- "${dev_blocked[$dev]:-0}" \
229
- "${dev_stale[$dev]:-0}"
230
- done
231
-
232
- echo ""
233
-
234
- # Build total summary parts
235
- local parts=()
236
- [[ $total_inprogress -gt 0 ]] && parts+=("$total_inprogress in progress")
237
- [[ $total_blocked -gt 0 ]] && parts+=("$total_blocked blocked")
238
- [[ $total_stale -gt 0 ]] && parts+=("$total_stale stale")
239
- [[ $total_open -gt 0 ]] && parts+=("$total_open open")
240
-
241
- local summary=""
242
- if [[ ${#parts[@]} -gt 0 ]]; then
243
- summary=" ($(IFS=', '; echo "${parts[*]}"))"
244
- fi
245
- echo "Total: $total_issues issues${summary}"
246
-
247
- # Stale section
248
- if [[ ${#stale_entries[@]} -gt 0 ]]; then
249
- echo ""
250
- echo "Stale assignments (>48h):"
251
- local entry
252
- for entry in "${stale_entries[@]}"; do
253
- IFS=$'\t' read -r _id _owner _title _age <<< "$entry"
254
- echo " ⚠ $_id [$_owner] — $_title ($_age)"
255
- done
256
- fi
257
-
258
- # Blocked section
259
- if [[ ${#blocked_entries[@]} -gt 0 ]]; then
260
- echo ""
261
- echo "Blocked issues:"
262
- local entry
263
- for entry in "${blocked_entries[@]}"; do
264
- IFS=$'\t' read -r _id _owner _title _deps <<< "$entry"
265
- echo " ⚠ $_id [$_owner] — $_title (blocked by $_deps)"
266
- done
267
- fi
268
- }
269
-
270
- # ── JSON Output ──────────────────────────────────────────────────────────
271
-
272
- _dashboard_json() {
273
- local total_issues="$1"
274
-
275
- # Build developers JSON
276
- local dev_json="{"
277
- local first=1
278
- local devs=()
279
- local dev
280
- for dev in "${!dev_open[@]}"; do
281
- devs+=("$dev")
282
- done
283
- IFS=$'\n' devs=($(sort <<< "${devs[*]}")); unset IFS
284
-
285
- for dev in "${devs[@]}"; do
286
- [[ $first -eq 0 ]] && dev_json+=","
287
- first=0
288
- dev_json+="\"$dev\":{\"open\":${dev_open[$dev]:-0},\"in_progress\":${dev_inprogress[$dev]:-0},\"blocked\":${dev_blocked[$dev]:-0},\"stale\":${dev_stale[$dev]:-0}}"
289
- done
290
- dev_json+="}"
291
-
292
- # Build stale JSON array
293
- local stale_json="["
294
- first=1
295
- local entry
296
- for entry in "${stale_entries[@]}"; do
297
- [[ $first -eq 0 ]] && stale_json+=","
298
- first=0
299
- IFS=$'\t' read -r _id _owner _title _age <<< "$entry"
300
- stale_json+="$(jq -n -c --arg id "$_id" --arg owner "$_owner" --arg title "$_title" --arg age "$_age" '{id:$id,owner:$owner,title:$title,age:$age}')"
301
- done
302
- stale_json+="]"
303
-
304
- # Build blocked JSON array
305
- local blocked_json="["
306
- first=1
307
- for entry in "${blocked_entries[@]}"; do
308
- [[ $first -eq 0 ]] && blocked_json+=","
309
- first=0
310
- IFS=$'\t' read -r _id _owner _title _deps <<< "$entry"
311
- blocked_json+="$(jq -n -c --arg id "$_id" --arg owner "$_owner" --arg title "$_title" --arg deps "$_deps" '{id:$id,owner:$owner,title:$title,blocked_by:$deps}')"
312
- done
313
- blocked_json+="]"
314
-
315
- echo "{\"developers\":$dev_json,\"total\":$total_issues,\"in_progress\":$total_inprogress,\"blocked\":$total_blocked,\"stale\":$total_stale,\"stale_entries\":$stale_json,\"blocked_entries\":$blocked_json}"
316
- }