forgedock 1.0.22 → 1.1.2

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 (184) hide show
  1. package/README.md +255 -211
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/cinema.mjs +320 -0
  4. package/bin/demo.mjs +92 -7
  5. package/bin/engine/phases.mjs +168 -0
  6. package/bin/engine/projector.mjs +32 -0
  7. package/bin/engine/reconcile.mjs +11 -0
  8. package/bin/engine/runlog.mjs +79 -0
  9. package/bin/engine/state.mjs +32 -0
  10. package/bin/engine-cli.mjs +222 -0
  11. package/bin/engine.mjs +138 -0
  12. package/bin/forgedock.mjs +896 -1540
  13. package/bin/hooks/interactive-engine.mjs +509 -0
  14. package/bin/hooks/pre-tool-use.mjs +498 -0
  15. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  16. package/bin/journey.mjs +841 -0
  17. package/bin/registry.mjs +139 -0
  18. package/bin/report.mjs +447 -0
  19. package/bin/runner.mjs +136 -20
  20. package/bin/settings-hook.mjs +346 -0
  21. package/bin/tui.mjs +6 -3
  22. package/commands/adopt.md +4 -1
  23. package/commands/analytics.md +4 -1
  24. package/commands/audit-agents.md +110 -27
  25. package/commands/audit.md +20 -1
  26. package/commands/autopilot.md +84 -12
  27. package/commands/changelog.md +2 -1
  28. package/commands/ci-audit.md +2 -1
  29. package/commands/cleanup.md +161 -1
  30. package/commands/compat-audit.md +2 -1
  31. package/commands/deploy-info.md +63 -4
  32. package/commands/design-bench.md +6 -1
  33. package/commands/design-render-critique-loop.md +2 -1
  34. package/commands/design.md +1 -0
  35. package/commands/diagnose.md +2 -1
  36. package/commands/explain.md +2 -1
  37. package/commands/forge-stats.md +122 -2
  38. package/commands/forgedock-init.md +1 -1
  39. package/commands/geo-audit.md +3 -0
  40. package/commands/incident-response.md +2 -1
  41. package/commands/issue.md +24 -3
  42. package/commands/milestone.md +77 -12
  43. package/commands/optimize.md +2 -1
  44. package/commands/orchestrate/config.md +67 -0
  45. package/commands/orchestrate/phase-1-resolve.md +159 -0
  46. package/commands/orchestrate/phase-2-triage.md +126 -0
  47. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  48. package/commands/orchestrate/phase-3-dependency.md +591 -0
  49. package/commands/orchestrate/phase-4-execution.md +962 -0
  50. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  51. package/commands/orchestrate/phase-6-report.md +198 -0
  52. package/commands/orchestrate/safety.md +62 -0
  53. package/commands/orchestrate.md +25 -2027
  54. package/commands/pipeline-health/config.md +34 -0
  55. package/commands/pipeline-health/phase-1-context.md +180 -0
  56. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  57. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  58. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  59. package/commands/pipeline-health/phase-5-report.md +284 -0
  60. package/commands/pipeline-health/phase-6-summary.md +52 -0
  61. package/commands/pipeline-health.md +25 -2401
  62. package/commands/pipeline-resume.md +24 -1
  63. package/commands/pipeline-status.md +65 -0
  64. package/commands/qa-sweep.md +4 -1
  65. package/commands/quality-gate.md +326 -42
  66. package/commands/replay.md +2 -1
  67. package/commands/review-pr-agents/api.md +135 -0
  68. package/commands/review-pr-agents/auth.md +137 -0
  69. package/commands/review-pr-agents/billing.md +101 -0
  70. package/commands/review-pr-agents/concurrency.md +145 -0
  71. package/commands/review-pr-agents/database.md +103 -0
  72. package/commands/review-pr-agents/frontend.md +89 -0
  73. package/commands/review-pr-agents/infra.md +385 -0
  74. package/commands/review-pr-agents/protocols.md +185 -0
  75. package/commands/review-pr-agents/scraper.md +185 -0
  76. package/commands/review-pr-agents/security.md +406 -0
  77. package/commands/review-pr-agents.md +45 -1722
  78. package/commands/review-pr-staging.md +196 -40
  79. package/commands/review-pr.md +612 -56
  80. package/commands/rollback.md +2 -1
  81. package/commands/scope.md +2 -1
  82. package/commands/security-audit.md +2 -1
  83. package/commands/signal-planner.md +623 -0
  84. package/commands/sync-ecosystem.md +1 -0
  85. package/commands/test-gate.md +17 -1
  86. package/commands/validate.md +2 -1
  87. package/commands/work-on/build/architect.md +28 -0
  88. package/commands/work-on/build/context.md +2 -0
  89. package/commands/work-on/build/implement.md +19 -5
  90. package/commands/work-on/build/validate.md +62 -6
  91. package/commands/work-on/build.md +187 -30
  92. package/commands/work-on/close.md +132 -5
  93. package/commands/work-on/decompose.md +19 -1
  94. package/commands/work-on/investigate.md +117 -2
  95. package/commands/work-on/review.md +48 -4
  96. package/commands/work-on-monolithic.md +8 -2
  97. package/commands/work-on.md +247 -7
  98. package/docs/BRAND.md +16 -5
  99. package/docs/CI.md +2 -2
  100. package/docs/CODEX.md +36 -1
  101. package/docs/CONFIG.md +44 -42
  102. package/docs/FORGE-PROTOCOL.md +15 -1064
  103. package/docs/anthropic-devrel-outreach.md +2 -2
  104. package/docs/articles/command-decomposition-token-savings.md +214 -0
  105. package/docs/articles/model-release-playbook.md +131 -0
  106. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  107. package/docs/claude-breakpoints.json +75 -5
  108. package/docs/design/abc-benchmark.md +1 -1
  109. package/docs/design/design-spec-schema.md +1 -1
  110. package/docs/design/divergent-generation.md +1 -1
  111. package/docs/design/eval-gate-runbook.md +178 -0
  112. package/docs/design/show-hn-draft.md +26 -1
  113. package/docs/design/token-cost-baseline-1298.md +59 -0
  114. package/docs/eval/README.md +117 -0
  115. package/docs/eval/corpus/seed-v1.json +6 -0
  116. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  117. package/docs/site/annotations-explained.md +24 -0
  118. package/docs/site/command-learning-path.md +51 -14
  119. package/docs/site/command-reference.md +339 -113
  120. package/docs/site/for-companies.md +95 -0
  121. package/docs/site/forge-annotation-protocol.md +12 -256
  122. package/docs/site/getting-started.md +58 -12
  123. package/docs/site/github-is-the-memory.md +113 -0
  124. package/docs/site/how-it-works.md +3 -13
  125. package/docs/site/index.md +10 -2
  126. package/docs/spec/eval-run-result.md +147 -0
  127. package/docs/spec/forge-protocol-v1.md +441 -18
  128. package/docs/spec/label-state-machine.md +55 -0
  129. package/docs/spec/review-protocol.md +158 -0
  130. package/docs/spec-graph-schema.md +28 -0
  131. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  132. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  133. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  134. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  135. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  136. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  137. package/examples/forgedock-demo/README.md +41 -22
  138. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  139. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  140. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  141. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  142. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  143. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  144. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  145. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  146. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  147. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  148. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  149. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  150. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  151. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  152. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  153. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  154. package/examples/forgedock-demo/src/db.js +36 -11
  155. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  156. package/examples/forgedock-demo/src/server.js +4 -1
  157. package/package.json +12 -4
  158. package/packages/protocol/LICENSE +21 -0
  159. package/packages/protocol/src/cli.js +135 -0
  160. package/packages/protocol/src/emit.js +146 -0
  161. package/packages/protocol/src/index.js +43 -0
  162. package/packages/protocol/src/parse.js +229 -0
  163. package/packages/protocol/src/types.js +171 -0
  164. package/packages/protocol/src/validate.js +122 -0
  165. package/scripts/bench-topology-cost.mjs +372 -0
  166. package/scripts/check-command-docs-drift.sh +198 -0
  167. package/scripts/check-protocol-restatements.sh +176 -0
  168. package/scripts/check-registry/README.md +72 -0
  169. package/scripts/check-registry/manifest.json +6 -0
  170. package/scripts/code-index.sh +454 -0
  171. package/scripts/conformance-check.mjs +168 -0
  172. package/scripts/doctor-pipeline-state.sh +484 -0
  173. package/scripts/eval-gate-baseline.json +17 -0
  174. package/scripts/eval-gate-scorecard.mjs +242 -0
  175. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  176. package/scripts/flaky-quarantine.sh +241 -0
  177. package/scripts/forge-annotation.sh +567 -0
  178. package/scripts/issue-dedup.sh +236 -0
  179. package/scripts/validate-annotation-node.mjs +233 -0
  180. package/scripts/verify-env-vars.sh +18 -2
  181. package/scripts/verify-host-headers.sh +4 -4
  182. package/scripts/verify-route-registration.sh +2 -2
  183. package/scripts/worktree-lifecycle.sh +202 -0
  184. package/templates/devdocs/agent/using-forgedock.md +14 -0
@@ -0,0 +1,484 @@
1
+ #!/usr/bin/env bash
2
+ # doctor-pipeline-state.sh — Deterministic pipeline stall detection
3
+ #
4
+ # Usage:
5
+ # doctor-pipeline-state.sh [--repo owner/repo] [--json] [--stuck-hours N]
6
+ #
7
+ # Options:
8
+ # --repo owner/repo Target repository (default: reads from forge.yaml)
9
+ # --json Emit machine-readable JSON instead of human summary
10
+ # --stuck-hours N Hours before an issue is considered stuck (default: 48)
11
+ #
12
+ # Output (human mode):
13
+ # One finding per stalled issue/PR, including:
14
+ # - What is wrong
15
+ # - Which invariant broke
16
+ # - Exact resume command (copy-pasteable)
17
+ #
18
+ # Output (JSON mode):
19
+ # { "findings": [ { "type", "severity", "issue", "label", "hours_stuck",
20
+ # "last_annotation", "resume_command", "detail" } ],
21
+ # "summary": { "total", "critical", "warning",
22
+ # "checks_skipped", "degraded": [ { "check", "issue", "reason" } ] } }
23
+ #
24
+ # "checks_skipped"/"degraded" surface I4/I5 checks that fail-open on a real
25
+ # `gh` API failure (rate limit, network blip, auth issue) rather than being
26
+ # treated as confirmed-missing (see #1531). A skip is NOT a finding — it does
27
+ # not affect "total"/"critical"/"warning" or the exit code — but a
28
+ # persistently non-zero "checks_skipped" across runs means those invariants
29
+ # are silently disabled and warrants investigating the underlying `gh` failure.
30
+ #
31
+ # Exit codes:
32
+ # 0 = no findings (pipeline healthy)
33
+ # 1 = findings present
34
+ # 2 = error (missing deps, bad args)
35
+ #
36
+ # Depends on: gh (GitHub CLI), jq, date
37
+ #
38
+ # Invariants checked:
39
+ # I1 Issues stuck in workflow:investigating beyond threshold
40
+ # I2 Issues stuck in workflow:building beyond threshold
41
+ # I3 Issues stuck in workflow:in-review beyond threshold
42
+ # I4 Issues in workflow:building with no FORGE:BUILDER annotation
43
+ # I5 Issues in workflow:in-review with no open PR
44
+ # I6 PRs open without a FORGE:CONTRACT or FORGE:BUILDER annotation
45
+ # I7 Orphaned worktree branches (local branches fix/* with no open PR)
46
+ #
47
+ # /pipeline-health should consume this script's JSON output rather than
48
+ # re-discovering state from scratch.
49
+
50
+ set -euo pipefail
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Constants
54
+ # ---------------------------------------------------------------------------
55
+ STUCK_HOURS_DEFAULT=48
56
+ TERMINAL_LABELS="workflow:merged workflow:invalid workflow:decomposed needs-human"
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Argument parsing
60
+ # ---------------------------------------------------------------------------
61
+ GH_REPO=""
62
+ JSON_MODE=false
63
+ STUCK_HOURS=$STUCK_HOURS_DEFAULT
64
+
65
+ while [[ $# -gt 0 ]]; do
66
+ case "$1" in
67
+ --repo)
68
+ GH_REPO="$2"; shift 2 ;;
69
+ --json)
70
+ JSON_MODE=true; shift ;;
71
+ --stuck-hours)
72
+ STUCK_HOURS="$2"; shift 2 ;;
73
+ -h|--help)
74
+ sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'
75
+ exit 0 ;;
76
+ *)
77
+ echo "ERROR: Unknown argument: $1" >&2
78
+ echo "Usage: doctor-pipeline-state.sh [--repo owner/repo] [--json] [--stuck-hours N]" >&2
79
+ exit 2 ;;
80
+ esac
81
+ done
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Dependency checks
85
+ # ---------------------------------------------------------------------------
86
+ for dep in gh jq; do
87
+ if ! command -v "$dep" &>/dev/null; then
88
+ echo "ERROR: Required dependency not found: $dep" >&2
89
+ exit 2
90
+ fi
91
+ done
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Resolve repo
95
+ # ---------------------------------------------------------------------------
96
+ if [ -z "$GH_REPO" ]; then
97
+ if [ -f "forge.yaml" ] && command -v yq &>/dev/null; then
98
+ GH_REPO=$(yq e '.project.owner + "/" + .project.repo' forge.yaml 2>/dev/null || true)
99
+ fi
100
+ if [ -z "$GH_REPO" ]; then
101
+ GH_REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || true)
102
+ fi
103
+ if [ -z "$GH_REPO" ]; then
104
+ echo "ERROR: Could not determine repository. Pass --repo owner/repo or run from a git repo." >&2
105
+ exit 2
106
+ fi
107
+ fi
108
+
109
+ GH_FLAG="-R $GH_REPO"
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Shared stderr sink for checks that must not merge stderr into a compared
113
+ # value (see I4/I5 below). Redirecting to this file (instead of `2>&1`) keeps
114
+ # a captured variable limited to stdout on success, while still preserving
115
+ # the diagnostic stderr text for the WARNING message on actual failure.
116
+ # ---------------------------------------------------------------------------
117
+ GH_STDERR_TMP=$(mktemp)
118
+ trap 'rm -f "$GH_STDERR_TMP"' EXIT
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Utility: ISO timestamp to hours-ago
122
+ # ---------------------------------------------------------------------------
123
+ hours_since() {
124
+ local ts="$1"
125
+ local now
126
+ now=$(date -u +%s)
127
+ # Accept both ISO 8601 with Z and with offset
128
+ local then
129
+ then=$(date -u -d "$ts" +%s 2>/dev/null || date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$ts" +%s 2>/dev/null || echo "$now")
130
+ echo $(( (now - then) / 3600 ))
131
+ }
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Utility: does a JSON array of label names contain any TERMINAL_LABELS entry?
135
+ # ---------------------------------------------------------------------------
136
+ has_terminal_label() {
137
+ local labels_json="$1"
138
+ local label
139
+ for label in $TERMINAL_LABELS; do
140
+ if echo "$labels_json" | jq -e --arg l "$label" 'index($l) != null' >/dev/null 2>&1; then
141
+ return 0
142
+ fi
143
+ done
144
+ return 1
145
+ }
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Findings accumulator
149
+ # ---------------------------------------------------------------------------
150
+ FINDINGS_JSON="[]"
151
+
152
+ add_finding() {
153
+ local type="$1"
154
+ local severity="$2"
155
+ local issue_num="$3"
156
+ local label="$4"
157
+ local hours_stuck="$5"
158
+ local last_annotation="$6"
159
+ local resume_command="$7"
160
+ local detail="$8"
161
+
162
+ local entry
163
+ entry=$(jq -n \
164
+ --arg type "$type" \
165
+ --arg severity "$severity" \
166
+ --arg issue "$issue_num" \
167
+ --arg label "$label" \
168
+ --argjson hours_stuck "$hours_stuck" \
169
+ --arg last_annotation "$last_annotation" \
170
+ --arg resume_command "$resume_command" \
171
+ --arg detail "$detail" \
172
+ '{type:$type,severity:$severity,issue:$issue,label:$label,
173
+ hours_stuck:$hours_stuck,last_annotation:$last_annotation,
174
+ resume_command:$resume_command,detail:$detail}')
175
+ FINDINGS_JSON=$(echo "$FINDINGS_JSON" | jq ". + [$entry]")
176
+ }
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Skipped-checks accumulator
180
+ #
181
+ # I4/I5 fail open on a real `gh` API failure (rate limit, network blip, auth
182
+ # issue) rather than treating it as confirmed-missing — this is intentional
183
+ # (see #1531: skip > false-CRITICAL). The skip previously logged only to
184
+ # stderr, which is invisible to /pipeline-health (it consumes --json stdout,
185
+ # not stderr). add_skip() records the same skip event into a JSON-tracked
186
+ # array so persistent degradation surfaces in the machine-readable contract.
187
+ # ---------------------------------------------------------------------------
188
+ SKIPPED_JSON="[]"
189
+
190
+ # bounded_reason: cap raw external-tool stderr before it enters the published
191
+ # JSON contract. `gh` does not normally print secrets to stderr, but verbose/
192
+ # debug tracing (e.g. GH_DEBUG) can emit multi-line output including request
193
+ # headers. Keep only the first line, capped at 300 chars, so the published
194
+ # degraded[].reason field can never carry unbounded/verbose diagnostic text.
195
+ # The full untruncated text still goes to the local `>&2` warning above each
196
+ # add_skip call — this only bounds the copy that gets published.
197
+ bounded_reason() {
198
+ local raw="${1:-}"
199
+ local first_line
200
+ # `|| true` guards against SIGPIPE (exit 141) under `set -euo pipefail`:
201
+ # head closes the pipe after the first line, so printf may be killed while
202
+ # still writing on large multi-line input. This keeps the helper self-safe
203
+ # regardless of whether the caller wraps it in a command substitution.
204
+ first_line=$(printf '%s\n' "$raw" | head -n1 || true)
205
+ if [ "${#first_line}" -gt 300 ]; then
206
+ printf '%s...[truncated]' "${first_line:0:300}"
207
+ else
208
+ printf '%s' "$first_line"
209
+ fi
210
+ }
211
+
212
+ add_skip() {
213
+ local check="$1"
214
+ local issue_num="$2"
215
+ local reason="$3"
216
+
217
+ local entry
218
+ entry=$(jq -n \
219
+ --arg check "$check" \
220
+ --arg issue "$issue_num" \
221
+ --arg reason "$reason" \
222
+ '{check:$check,issue:$issue,reason:$reason}')
223
+ SKIPPED_JSON=$(echo "$SKIPPED_JSON" | jq ". + [$entry]")
224
+ }
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # I1: Issues stuck in workflow:investigating
228
+ # ---------------------------------------------------------------------------
229
+ # Distinguish a confirmed-empty list from a gh API failure (rate limit, network
230
+ # blip, auth issue) — an API failure must NOT be treated as "no issues found".
231
+ if ! INVESTIGATING=$(gh issue list $GH_FLAG \
232
+ --state open --label "workflow:investigating" \
233
+ --limit 100 \
234
+ --json number,title,updatedAt,labels 2>"$GH_STDERR_TMP"); then
235
+ echo "WARNING: gh issue list failed for workflow:investigating — I1 check inconclusive, skipping (not treated as empty): $(cat "$GH_STDERR_TMP")" >&2
236
+ add_skip "I1" "all" "gh issue list failed: $(bounded_reason "$(cat "$GH_STDERR_TMP")")"
237
+ INVESTIGATING="[]"
238
+ fi
239
+
240
+ while IFS= read -r row; do
241
+ num=$(echo "$row" | jq -r '.number')
242
+ labels=$(echo "$row" | jq -c '[.labels[].name]')
243
+ if has_terminal_label "$labels"; then
244
+ continue
245
+ fi
246
+ updated=$(echo "$row" | jq -r '.updatedAt')
247
+ hours=$(hours_since "$updated")
248
+ if [ "$hours" -ge "$STUCK_HOURS" ]; then
249
+ add_finding \
250
+ "stuck_investigating" "warning" \
251
+ "$num" "workflow:investigating" "$hours" \
252
+ "FORGE:INVESTIGATOR" \
253
+ "/work-on $num --resume" \
254
+ "Issue #$num stuck in workflow:investigating for ${hours}h (threshold: ${STUCK_HOURS}h). Last annotation expected: FORGE:INVESTIGATOR. Resume with /work-on $num --resume"
255
+ fi
256
+ done < <(echo "$INVESTIGATING" | jq -c '.[]')
257
+
258
+ # ---------------------------------------------------------------------------
259
+ # I2: Issues stuck in workflow:building
260
+ # ---------------------------------------------------------------------------
261
+ # Distinguish a confirmed-empty list from a gh API failure — see I1 above.
262
+ if ! BUILDING=$(gh issue list $GH_FLAG \
263
+ --state open --label "workflow:building" \
264
+ --limit 100 \
265
+ --json number,title,updatedAt,labels 2>"$GH_STDERR_TMP"); then
266
+ echo "WARNING: gh issue list failed for workflow:building — I2 check inconclusive, skipping (not treated as empty): $(cat "$GH_STDERR_TMP")" >&2
267
+ add_skip "I2" "all" "gh issue list failed: $(bounded_reason "$(cat "$GH_STDERR_TMP")")"
268
+ BUILDING="[]"
269
+ fi
270
+
271
+ while IFS= read -r row; do
272
+ num=$(echo "$row" | jq -r '.number')
273
+ labels=$(echo "$row" | jq -c '[.labels[].name]')
274
+ if has_terminal_label "$labels"; then
275
+ continue
276
+ fi
277
+ updated=$(echo "$row" | jq -r '.updatedAt')
278
+ hours=$(hours_since "$updated")
279
+ if [ "$hours" -ge "$STUCK_HOURS" ]; then
280
+ add_finding \
281
+ "stuck_building" "warning" \
282
+ "$num" "workflow:building" "$hours" \
283
+ "FORGE:BUILDER" \
284
+ "/work-on $num --resume" \
285
+ "Issue #$num stuck in workflow:building for ${hours}h (threshold: ${STUCK_HOURS}h). Last annotation expected: FORGE:BUILDER. Resume with /work-on $num --resume"
286
+ fi
287
+ done < <(echo "$BUILDING" | jq -c '.[]')
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # I3: Issues stuck in workflow:in-review
291
+ # ---------------------------------------------------------------------------
292
+ # Distinguish a confirmed-empty list from a gh API failure — see I1 above.
293
+ if ! IN_REVIEW=$(gh issue list $GH_FLAG \
294
+ --state open --label "workflow:in-review" \
295
+ --limit 100 \
296
+ --json number,title,updatedAt,labels 2>"$GH_STDERR_TMP"); then
297
+ echo "WARNING: gh issue list failed for workflow:in-review — I3 check inconclusive, skipping (not treated as empty): $(cat "$GH_STDERR_TMP")" >&2
298
+ add_skip "I3" "all" "gh issue list failed: $(bounded_reason "$(cat "$GH_STDERR_TMP")")"
299
+ IN_REVIEW="[]"
300
+ fi
301
+
302
+ while IFS= read -r row; do
303
+ num=$(echo "$row" | jq -r '.number')
304
+ labels=$(echo "$row" | jq -c '[.labels[].name]')
305
+ if has_terminal_label "$labels"; then
306
+ continue
307
+ fi
308
+ updated=$(echo "$row" | jq -r '.updatedAt')
309
+ hours=$(hours_since "$updated")
310
+ if [ "$hours" -ge "$STUCK_HOURS" ]; then
311
+ add_finding \
312
+ "stuck_in_review" "warning" \
313
+ "$num" "workflow:in-review" "$hours" \
314
+ "FORGE:TRAJECTORY" \
315
+ "/review-pr $num" \
316
+ "Issue #$num stuck in workflow:in-review for ${hours}h (threshold: ${STUCK_HOURS}h). Resume review with /review-pr (find associated PR first)"
317
+ fi
318
+ done < <(echo "$IN_REVIEW" | jq -c '.[]')
319
+
320
+ # ---------------------------------------------------------------------------
321
+ # I4: Issues in workflow:building with no FORGE:BUILDER annotation
322
+ # ---------------------------------------------------------------------------
323
+ while IFS= read -r row; do
324
+ num=$(echo "$row" | jq -r '.number')
325
+ # Check for FORGE:BUILDER annotation in issue comments.
326
+ # Distinguish a confirmed-absent annotation from a gh API failure (rate limit,
327
+ # network blip, auth issue) — an API failure must NOT be treated as confirmed-missing.
328
+ if ! has_builder=$(gh issue view "$num" $GH_FLAG \
329
+ --json comments \
330
+ --jq '[.comments[].body | contains("FORGE:BUILDER")] | any' 2>"$GH_STDERR_TMP"); then
331
+ echo "WARNING: gh issue view failed for #$num — I4 check inconclusive, skipping (not treated as missing): $(cat "$GH_STDERR_TMP")" >&2
332
+ add_skip "I4" "$num" "gh issue view failed: $(bounded_reason "$(cat "$GH_STDERR_TMP")")"
333
+ continue
334
+ fi
335
+ if [ "$has_builder" = "false" ]; then
336
+ add_finding \
337
+ "missing_annotation" "critical" \
338
+ "$num" "workflow:building" "0" \
339
+ "none" \
340
+ "/work-on $num --resume" \
341
+ "Issue #$num is labeled workflow:building but has no FORGE:BUILDER annotation. The builder phase either never ran or failed before writing its annotation. Invariant I4 broken. Resume: /work-on $num --resume"
342
+ fi
343
+ done < <(echo "$BUILDING" | jq -c '.[]')
344
+
345
+ # ---------------------------------------------------------------------------
346
+ # I5: Issues in workflow:in-review with no open PR
347
+ # ---------------------------------------------------------------------------
348
+ while IFS= read -r row; do
349
+ num=$(echo "$row" | jq -r '.number')
350
+ # Distinguish a confirmed-absent PR from a gh API failure (rate limit, network
351
+ # blip, auth issue) — an API failure must NOT be treated as confirmed-missing.
352
+ if ! pr_count=$(gh pr list $GH_FLAG \
353
+ --state open \
354
+ --search "closes #$num" \
355
+ --json number \
356
+ --jq '. | length' 2>"$GH_STDERR_TMP"); then
357
+ echo "WARNING: gh pr list failed for #$num — I5 check inconclusive, skipping (not treated as missing): $(cat "$GH_STDERR_TMP")" >&2
358
+ add_skip "I5" "$num" "gh pr list failed: $(bounded_reason "$(cat "$GH_STDERR_TMP")")"
359
+ continue
360
+ fi
361
+ if [ "$pr_count" -eq 0 ]; then
362
+ add_finding \
363
+ "missing_pr" "critical" \
364
+ "$num" "workflow:in-review" "0" \
365
+ "FORGE:TRAJECTORY" \
366
+ "/work-on $num --resume" \
367
+ "Issue #$num is labeled workflow:in-review but no open PR references it. Invariant I5 broken. Resume: /work-on $num --resume"
368
+ fi
369
+ done < <(echo "$IN_REVIEW" | jq -c '.[]')
370
+
371
+ # ---------------------------------------------------------------------------
372
+ # I6: PRs open without FORGE annotations
373
+ # ---------------------------------------------------------------------------
374
+ # Distinguish a confirmed-empty list from a gh API failure — see I1 above.
375
+ if ! OPEN_PRS=$(gh pr list $GH_FLAG \
376
+ --state open \
377
+ --limit 50 \
378
+ --json number,title,body,createdAt 2>"$GH_STDERR_TMP"); then
379
+ echo "WARNING: gh pr list failed for open PRs — I6 check inconclusive, skipping (not treated as empty): $(cat "$GH_STDERR_TMP")" >&2
380
+ add_skip "I6" "all" "gh pr list failed: $(bounded_reason "$(cat "$GH_STDERR_TMP")")"
381
+ OPEN_PRS="[]"
382
+ fi
383
+
384
+ while IFS= read -r row; do
385
+ pr_num=$(echo "$row" | jq -r '.number')
386
+ body=$(echo "$row" | jq -r '.body // ""')
387
+ created=$(echo "$row" | jq -r '.createdAt')
388
+ hours=$(hours_since "$created")
389
+ # Only flag PRs older than 1 hour to avoid false positives on brand-new PRs
390
+ if [ "$hours" -lt 1 ]; then
391
+ continue
392
+ fi
393
+ has_forge=$(echo "$body" | grep -c "FORGE:" || true)
394
+ if [ "$has_forge" -eq 0 ]; then
395
+ add_finding \
396
+ "pr_missing_annotations" "warning" \
397
+ "PR#$pr_num" "open_pr" "$hours" \
398
+ "none" \
399
+ "gh pr view $pr_num $GH_FLAG" \
400
+ "PR #$pr_num is open but has no FORGE: annotations in its body. Either it was created outside the pipeline or the annotation was lost. Invariant I6: pipeline PRs must have FORGE:CONTRACT or FORGE:BUILDER. Inspect: gh pr view $pr_num $GH_FLAG"
401
+ fi
402
+ done < <(echo "$OPEN_PRS" | jq -c '.[]')
403
+
404
+ # ---------------------------------------------------------------------------
405
+ # Summary
406
+ # ---------------------------------------------------------------------------
407
+ TOTAL=$(echo "$FINDINGS_JSON" | jq 'length')
408
+ CRITICAL=$(echo "$FINDINGS_JSON" | jq '[.[] | select(.severity == "critical")] | length')
409
+ WARNING=$(echo "$FINDINGS_JSON" | jq '[.[] | select(.severity == "warning")] | length')
410
+ CHECKS_SKIPPED=$(echo "$SKIPPED_JSON" | jq 'length')
411
+
412
+ SUMMARY_JSON=$(jq -n \
413
+ --argjson total "$TOTAL" \
414
+ --argjson critical "$CRITICAL" \
415
+ --argjson warning "$WARNING" \
416
+ --argjson checks_skipped "$CHECKS_SKIPPED" \
417
+ --argjson degraded "$SKIPPED_JSON" \
418
+ --arg repo "$GH_REPO" \
419
+ --arg stuck_hours "$STUCK_HOURS" \
420
+ '{repo:$repo,stuck_hours_threshold:($stuck_hours|tonumber),
421
+ total:$total,critical:$critical,warning:$warning,
422
+ checks_skipped:$checks_skipped,degraded:$degraded}')
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # Output
426
+ # ---------------------------------------------------------------------------
427
+ if $JSON_MODE; then
428
+ jq -n \
429
+ --argjson findings "$FINDINGS_JSON" \
430
+ --argjson summary "$SUMMARY_JSON" \
431
+ '{findings:$findings,summary:$summary}'
432
+ [ "$TOTAL" -eq 0 ] && exit 0 || exit 1
433
+ fi
434
+
435
+ # Human-readable output
436
+ echo ""
437
+ echo "ForgeDock Pipeline State Doctor"
438
+ echo "Repo: $GH_REPO | Stuck threshold: ${STUCK_HOURS}h"
439
+ echo "────────────────────────────────────────────────────────────"
440
+
441
+ if [ "$CHECKS_SKIPPED" -gt 0 ]; then
442
+ echo " NOTE: $CHECKS_SKIPPED check(s) skipped due to gh API failures (see --json 'degraded' for detail)."
443
+ fi
444
+
445
+ if [ "$TOTAL" -eq 0 ]; then
446
+ if [ "$CHECKS_SKIPPED" -gt 0 ]; then
447
+ echo " No stalls detected in checks that ran, but $CHECKS_SKIPPED check(s) were skipped"
448
+ echo " due to gh API failures — pipeline health is INCONCLUSIVE, not confirmed healthy."
449
+ echo ""
450
+ exit 0
451
+ fi
452
+ echo " No stalls detected. Pipeline is healthy."
453
+ echo ""
454
+ exit 0
455
+ fi
456
+
457
+ echo " Found $TOTAL finding(s): $CRITICAL critical, $WARNING warning"
458
+ echo ""
459
+
460
+ while IFS= read -r finding; do
461
+ severity=$(echo "$finding" | jq -r '.severity')
462
+ type=$(echo "$finding" | jq -r '.type')
463
+ issue=$(echo "$finding" | jq -r '.issue')
464
+ hours=$(echo "$finding" | jq -r '.hours_stuck')
465
+ detail=$(echo "$finding" | jq -r '.detail')
466
+ resume=$(echo "$finding" | jq -r '.resume_command')
467
+
468
+ if [ "$severity" = "critical" ]; then
469
+ prefix="[CRITICAL]"
470
+ else
471
+ prefix="[WARNING] "
472
+ fi
473
+
474
+ echo " $prefix $type — $issue"
475
+ echo " Detail: $detail"
476
+ echo " Resume: $resume"
477
+ echo ""
478
+ done < <(echo "$FINDINGS_JSON" | jq -c '.[]')
479
+
480
+ echo "────────────────────────────────────────────────────────────"
481
+ echo " Run with --json for machine-readable output."
482
+ echo " /pipeline-health consumes this output for LLM analysis."
483
+ echo ""
484
+ exit 1
@@ -0,0 +1,17 @@
1
+ {
2
+ "schema_version": "1",
3
+ "run_id": "seed-placeholder",
4
+ "generated_at": "2026-07-04T00:00:00.000Z",
5
+ "corpus_size": 5,
6
+ "issues_run": 5,
7
+ "successful_runs": 4,
8
+ "failed_runs": 1,
9
+ "success_rate_pct": 80,
10
+ "mean_wall_clock_ms": null,
11
+ "median_wall_clock_ms": null,
12
+ "total_intervention_count": 1,
13
+ "cost": null,
14
+ "run_mode": "subset",
15
+ "spec_sha": null,
16
+ "_note": "Placeholder baseline — replace with a real harness run once #1285 is complete. Tracked in: https://github.com/RapierCraftStudios/ForgeDock/issues/1285"
17
+ }