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.
- package/README.md +255 -211
- package/bin/batch-runner.mjs +339 -0
- package/bin/cinema.mjs +320 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +168 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +32 -0
- package/bin/engine-cli.mjs +222 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +896 -1540
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +841 -0
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +136 -20
- package/bin/settings-hook.mjs +346 -0
- package/bin/tui.mjs +6 -3
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +84 -12
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +63 -4
- package/commands/design-bench.md +6 -1
- package/commands/design-render-critique-loop.md +2 -1
- package/commands/design.md +1 -0
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/forge-stats.md +122 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +24 -3
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2027
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +326 -42
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +612 -56
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +19 -5
- package/commands/work-on/build/validate.md +62 -6
- package/commands/work-on/build.md +187 -30
- package/commands/work-on/close.md +132 -5
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +48 -4
- package/commands/work-on-monolithic.md +8 -2
- package/commands/work-on.md +247 -7
- package/docs/BRAND.md +16 -5
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +44 -42
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +117 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +339 -113
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +58 -12
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +10 -2
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +12 -4
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +229 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
- 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
|
+
}
|