forgedock 1.8.1 → 1.8.3
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/bin/engine/admission.mjs +57 -1
- package/bin/engine/admission.test.mjs +60 -0
- package/commands/analytics.md +3 -1
- package/commands/audit-agents.md +3 -1
- package/commands/audit.md +10 -8
- package/commands/autopilot.md +16 -14
- package/commands/ci-audit.md +4 -2
- package/commands/cleanup.md +1 -1
- package/commands/deploy-pr.md +31 -11
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +1 -0
- package/commands/geo-audit.md +3 -1
- package/commands/incident-response.md +3 -1
- package/commands/issue.md +4 -2
- package/commands/milestone.md +8 -6
- package/commands/orchestrate/config.md +3 -2
- package/commands/orchestrate/phase-1-resolve.md +57 -12
- package/commands/orchestrate/phase-2-triage.md +1 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -0
- package/commands/orchestrate/phase-3-dependency.md +17 -5
- package/commands/orchestrate/phase-4-execution.md +625 -78
- package/commands/orchestrate/phase-5-cleanup.md +5 -2
- package/commands/orchestrate/phase-6-report.md +8 -3
- package/commands/orchestrate/safety.md +1 -0
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/config.md +1 -0
- package/commands/pipeline-health/phase-1-context.md +1 -0
- package/commands/pipeline-health/phase-2-metrics.md +1 -0
- package/commands/pipeline-health/phase-3-analyze.md +1 -0
- package/commands/pipeline-health/phase-4-proposals.md +7 -4
- package/commands/pipeline-health/phase-5-report.md +5 -2
- package/commands/pipeline-health/phase-6-summary.md +1 -0
- package/commands/pipeline-health.md +9 -7
- package/commands/pipeline-resume.md +4 -2
- package/commands/qa-sweep.md +3 -1
- package/commands/quality-gate.md +4 -3
- package/commands/recover-orphans.md +219 -13
- package/commands/review-pr-agents/api.md +1 -0
- package/commands/review-pr-agents/auth.md +1 -0
- package/commands/review-pr-agents/billing.md +1 -0
- package/commands/review-pr-agents/concurrency.md +1 -0
- package/commands/review-pr-agents/database.md +1 -0
- package/commands/review-pr-agents/frontend.md +1 -0
- package/commands/review-pr-agents/infra.md +1 -0
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents/scraper.md +1 -0
- package/commands/review-pr-agents/security.md +1 -0
- package/commands/review-pr-agents/spec-cli.md +1 -0
- package/commands/review-pr-agents.md +10 -4
- package/commands/review-pr-staging.md +14 -5
- package/commands/review-pr.md +187 -19
- package/commands/rollback.md +3 -1
- package/commands/security-audit.md +3 -1
- package/commands/signal-planner.md +6 -4
- package/commands/sync-ecosystem.md +4 -2
- package/commands/test-gate.md +4 -2
- package/commands/upgrade-deps.md +3 -1
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +82 -19
- package/commands/work-on/build/context.md +62 -18
- package/commands/work-on/build/implement.md +63 -18
- package/commands/work-on/build/validate.md +241 -60
- package/commands/work-on/build.md +345 -146
- package/commands/work-on/close.md +618 -246
- package/commands/work-on/decompose.md +240 -41
- package/commands/work-on/investigate.md +378 -61
- package/commands/work-on/remediate.md +43 -9
- package/commands/work-on/review.md +241 -29
- package/commands/work-on.md +180 -2014
- package/docs/CODEX.md +2 -0
- package/docs/CONFIG.md +11 -0
- package/package.json +1 -1
- package/scripts/build-spec-graph.mjs +11 -4
- package/scripts/check-spec-markers.sh +21 -1
- package/scripts/checkbox-sections.test.sh +8 -23
- package/scripts/extract-affected-files.sh +5 -2
- package/scripts/extract-affected-files.test.sh +27 -0
- package/scripts/forge-root.test.sh +176 -0
- package/scripts/is-docs-only.sh +31 -0
- package/scripts/is-docs-only.test.sh +64 -0
- package/scripts/lint-dispatch-prompt.sh +217 -0
- package/scripts/lint-dispatch-prompt.test.sh +307 -0
- package/scripts/validate-spec-graph.sh +3 -2
- package/scripts/verify-phase-trail.sh +282 -0
- package/scripts/verify-phase-trail.test.sh +298 -0
- package/scripts/wait-ci-green.sh +98 -0
- package/scripts/wait-ci-green.test.sh +99 -0
- package/commands/work-on-monolithic.md +0 -326
|
@@ -7,13 +7,15 @@ argument-hint: "[--dry-run | --since <hours> | --issue <number>]"
|
|
|
7
7
|
|
|
8
8
|
# /recover-orphans — Pipeline Orphan Recovery
|
|
9
9
|
|
|
10
|
+
> **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
|
|
11
|
+
|
|
10
12
|
**Input**: $ARGUMENTS
|
|
11
13
|
|
|
12
14
|
Scan ALL open issues with intermediate workflow labels for orphaned state — issues where the agent died mid-pipeline (context expired, rate-limited, crashed) and no active agent is continuing. Diagnose each orphan's actual GitHub state and apply the appropriate recovery action.
|
|
13
15
|
|
|
14
16
|
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet". Fallback: `model: "opus"` if rate-limited.
|
|
15
17
|
**NEVER use plan mode (EnterPlanMode).**
|
|
16
|
-
**NEVER use the Agent tool** — recover-orphans re-enters the pipeline via `Skill(skill="work-on", ...)` and `Skill(skill="review-pr", ...)` only.
|
|
18
|
+
**NEVER use the Agent tool** — recover-orphans re-enters the pipeline via `Skill(skill="{FORGE_SKILL_PREFIX}work-on", ...)` and `Skill(skill="{FORGE_SKILL_PREFIX}review-pr", ...)` only.
|
|
17
19
|
|
|
18
20
|
<!-- FORGE:SPEC_LOADED — recover-orphans.md loaded and active. Agent is bound by this spec. -->
|
|
19
21
|
|
|
@@ -171,7 +173,6 @@ for NUM in $ORPHAN_LIST; do
|
|
|
171
173
|
DIAG_REASON[$NUM]="Already in terminal state: $ISSUE_LABELS"
|
|
172
174
|
continue
|
|
173
175
|
fi
|
|
174
|
-
|
|
175
176
|
# Check for merged PR referencing this issue
|
|
176
177
|
MERGED_PR=$(gh pr list ${GH_FLAG} \
|
|
177
178
|
--state merged \
|
|
@@ -198,6 +199,15 @@ for NUM in $ORPHAN_LIST; do
|
|
|
198
199
|
continue
|
|
199
200
|
fi
|
|
200
201
|
|
|
202
|
+
# forge#3148: checked after the merged-PR label cleanup above, so a merged orphan is still closed out.
|
|
203
|
+
# An escalated orphan is waiting on a human — re-running a recovery action every sweep
|
|
204
|
+
# (e.g. re-invoking /review-pr against an unrepaired phase trail) only repeats the same refusal.
|
|
205
|
+
if echo ", $ISSUE_LABELS," | grep -q ", needs-human,"; then
|
|
206
|
+
DIAG_ACTION[$NUM]="skip"
|
|
207
|
+
DIAG_REASON[$NUM]="Escalated (needs-human) — waiting on a human"
|
|
208
|
+
continue
|
|
209
|
+
fi
|
|
210
|
+
|
|
201
211
|
# Check for an open PR on any branch associated with this issue
|
|
202
212
|
# Strategy: look for branches with the issue number in the name
|
|
203
213
|
ISSUE_BRANCH=$(git ls-remote --heads origin 2>/dev/null | grep "/$NUM" | sed 's|.*refs/heads/||' | head -1)
|
|
@@ -307,6 +317,117 @@ done
|
|
|
307
317
|
|
|
308
318
|
For each diagnosed issue, apply the recovery action. All mutating actions are skipped when `DRY_RUN=true`.
|
|
309
319
|
|
|
320
|
+
**Claim before re-entering the pipeline** (forge#3158): the `review-pr` first-refusal resume and `create-pr` re-enter `/work-on` inline. A repeated or concurrent sweep, or a live `/orchestrate` agent, could otherwise run the pipeline twice on one issue. Every inline `/work-on` resume MUST go through `claim_orphan` first and run inside `resume_orphan_inline`, which releases the claim on every exit. A sweep is not an orchestrator, so it takes an issue-scoped `FORGE:RECOVERY_CLAIM` marker (visible to other sweeps) and defers to any live orchestrator signal (a fresh `FORGE:HEARTBEAT`, which `/work-on --under-orchestration` posts at every phase entry).
|
|
321
|
+
|
|
322
|
+
**Claim refresh and guaranteed release** (forge#3171): liveness is judged from the claim comment's `updated_at`, and an inline resume posts no `FORGE:HEARTBEAT` (that is gated on `--under-orchestration`, which sweeps do not pass). So the claim is kept alive by editing it: `claim_orphan` records the claim comment id, and `start_claim_keepalive` runs `refresh_orphan` in a background loop every `RECOVERY_CLAIM_TTL_MIN / 3` minutes while the resume runs. `refresh_orphan` PATCHes the claim body, keeping the `**Sweep: id**` line and appending a changing `Refreshed: <UTC>` line, so `updated_at` always advances. Refresh is best-effort; if the keepalive is lost the claim degrades to the plain TTL behavior. `resume_orphan_inline` registers `trap ... release_orphan ... EXIT` in a subshell, so the claim is released always, on success and when the `Skill(...)` call fails, and `release_orphan` stops the keepalive first and is idempotent (it never posts the release marker twice). The keepalive and the `Skill(...)` call belong to the same sweep run. DRY_RUN makes every one of these helpers a no-op.
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
RECOVERY_CLAIM_TTL_MIN="${RECOVERY_CLAIM_TTL_MIN:-30}" # a claim or heartbeat older than this is treated as dead
|
|
326
|
+
SWEEP_ID="sweep-$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
|
327
|
+
|
|
328
|
+
# claim_orphan <issue> — returns 0 if this sweep now holds the claim, 1 if it must skip (reason in CLAIM_SKIP_REASON).
|
|
329
|
+
# Fails closed: an unreadable comment list (initial read or race-check re-read) is treated as "held by someone else".
|
|
330
|
+
# Both reads run under pipefail in a subshell: the pipeline status would otherwise be jq's, and a gh failure
|
|
331
|
+
# (rate limit, 403) would yield "[]" with exit 0, indistinguishable from "no live holder".
|
|
332
|
+
claim_orphan() {
|
|
333
|
+
local num="$1" now cutoff comments live resp
|
|
334
|
+
[ "$DRY_RUN" = "true" ] && return 0 # dry-run: no claim comment is posted (callers already skip dry-run resumes)
|
|
335
|
+
now=$(date -u +%s); cutoff=$(( now - RECOVERY_CLAIM_TTL_MIN * 60 ))
|
|
336
|
+
comments=$(set -o pipefail; gh api --paginate "repos/${GH_REPO}/issues/${num}/comments" 2>/dev/null \
|
|
337
|
+
| jq -s 'add // []') || { CLAIM_SKIP_REASON="could not read comments to check for a live holder"; return 1; }
|
|
338
|
+
|
|
339
|
+
# Live orchestrator signal: a recent heartbeat from /work-on --under-orchestration, or a recent claim of any sweep.
|
|
340
|
+
# A claim is "released" only by a later RELEASED marker naming its sweep id.
|
|
341
|
+
live=$(echo "$comments" | jq --argjson cutoff "$cutoff" '
|
|
342
|
+
([.[] | select(.body | contains("FORGE:RECOVERY_CLAIM_RELEASED")) | .body | capture("Sweep: (?<id>[^ \n]+)").id]) as $rel
|
|
343
|
+
| [ .[] | select(
|
|
344
|
+
((.body | contains("FORGE:HEARTBEAT")) or (.body | contains("<!-- FORGE:RECOVERY_CLAIM -->")))
|
|
345
|
+
and ((.body | contains("FORGE:RECOVERY_CLAIM_RELEASED")) | not)
|
|
346
|
+
and ((.updated_at | fromdateiso8601) >= $cutoff)
|
|
347
|
+
and ((.body | capture("Sweep: (?<id>[^ \n]+)")? // {id:""}).id as $sid | ($rel | index($sid)) == null)
|
|
348
|
+
) ] | length') || live=1
|
|
349
|
+
if [ "${live:-1}" -gt 0 ]; then
|
|
350
|
+
CLAIM_SKIP_REASON="live holder (heartbeat or unreleased recovery claim within ${RECOVERY_CLAIM_TTL_MIN}m)"
|
|
351
|
+
return 1
|
|
352
|
+
fi
|
|
353
|
+
|
|
354
|
+
# Post via the API so the comment id is known: refresh_orphan needs it. The Sweep line must stay line 2.
|
|
355
|
+
CLAIM_BODY="<!-- FORGE:RECOVERY_CLAIM -->
|
|
356
|
+
**Sweep: ${SWEEP_ID}**
|
|
357
|
+
Holder: /recover-orphans — resuming /work-on #${num}. Other sweeps and dispatchers: do not re-enter this issue until the matching release marker is posted."
|
|
358
|
+
resp=$(gh api -X POST "repos/${GH_REPO}/issues/${num}/comments" -f body="$CLAIM_BODY" --jq '.id' 2>/dev/null) \
|
|
359
|
+
&& [ -n "$resp" ] || { CLAIM_SKIP_REASON="could not post FORGE:RECOVERY_CLAIM"; return 1; }
|
|
360
|
+
CLAIM_COMMENT_ID="$resp"; CLAIM_HELD_NUM="$num" # set before the race check so release_orphan covers the race-lost paths
|
|
361
|
+
|
|
362
|
+
# Race check: re-read; the earliest unreleased claim wins. Losing means another sweep claimed first.
|
|
363
|
+
local first
|
|
364
|
+
first=$(set -o pipefail; gh api --paginate "repos/${GH_REPO}/issues/${num}/comments" 2>/dev/null | jq -rs --argjson cutoff "$cutoff" '
|
|
365
|
+
add // [] | ([.[] | select(.body | contains("FORGE:RECOVERY_CLAIM_RELEASED")) | .body | capture("Sweep: (?<id>[^ \n]+)").id]) as $rel
|
|
366
|
+
| [.[] | select(.body | contains("<!-- FORGE:RECOVERY_CLAIM -->"))
|
|
367
|
+
| select((.updated_at | fromdateiso8601) >= $cutoff)
|
|
368
|
+
| select((.body | capture("Sweep: (?<id>[^ \n]+)").id) as $sid | ($rel | index($sid)) == null)]
|
|
369
|
+
| first | .body // ""' | sed -n 's/^\*\*Sweep: \(.*\)\*\*$/\1/p') \
|
|
370
|
+
|| { release_orphan "$num"; CLAIM_SKIP_REASON="could not re-read comments to confirm the claim race"; return 1; }
|
|
371
|
+
if [ "$first" != "$SWEEP_ID" ]; then
|
|
372
|
+
release_orphan "$num"
|
|
373
|
+
CLAIM_SKIP_REASON="lost claim race to ${first:-unknown}"
|
|
374
|
+
return 1
|
|
375
|
+
fi
|
|
376
|
+
return 0
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
# refresh_orphan <issue> — best-effort: edit the claim comment so its updated_at advances. The body must change
|
|
380
|
+
# every time (an identical-body edit may not bump updated_at). Never carries FORGE:HEARTBEAT or the RELEASED marker.
|
|
381
|
+
refresh_orphan() {
|
|
382
|
+
[ "$DRY_RUN" = "true" ] && return 0
|
|
383
|
+
[ -n "${CLAIM_COMMENT_ID:-}" ] && [ "${CLAIM_HELD_NUM:-}" = "$1" ] || return 0
|
|
384
|
+
gh api -X PATCH "repos/${GH_REPO}/issues/comments/${CLAIM_COMMENT_ID}" \
|
|
385
|
+
-f body="${CLAIM_BODY}
|
|
386
|
+
Refreshed: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >/dev/null 2>&1 || true
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
# start_claim_keepalive <issue> — non-blocking background refresh every TTL/3 (strictly under the TTL).
|
|
390
|
+
start_claim_keepalive() {
|
|
391
|
+
[ "$DRY_RUN" = "true" ] && return 0
|
|
392
|
+
stop_claim_keepalive
|
|
393
|
+
local num="$1" interval=$(( RECOVERY_CLAIM_TTL_MIN * 60 / 3 ))
|
|
394
|
+
[ "$interval" -ge 1 ] || interval=1
|
|
395
|
+
( while sleep "$interval"; do refresh_orphan "$num"; done ) >/dev/null 2>&1 &
|
|
396
|
+
KEEPALIVE_PID=$!
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
stop_claim_keepalive() {
|
|
400
|
+
[ -n "${KEEPALIVE_PID:-}" ] || return 0
|
|
401
|
+
pkill -P "$KEEPALIVE_PID" 2>/dev/null || true # the pending sleep
|
|
402
|
+
kill "$KEEPALIVE_PID" 2>/dev/null || true
|
|
403
|
+
wait "$KEEPALIVE_PID" 2>/dev/null || true
|
|
404
|
+
unset KEEPALIVE_PID
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
# release_orphan <issue> — stops the keepalive first; idempotent (the release marker is posted at most once per claim).
|
|
408
|
+
release_orphan() {
|
|
409
|
+
[ "$DRY_RUN" = "true" ] && return 0
|
|
410
|
+
stop_claim_keepalive
|
|
411
|
+
[ "${CLAIM_HELD_NUM:-}" = "$1" ] || return 0
|
|
412
|
+
unset CLAIM_HELD_NUM CLAIM_COMMENT_ID
|
|
413
|
+
gh issue comment "$1" ${GH_FLAG} --body "<!-- FORGE:RECOVERY_CLAIM_RELEASED -->
|
|
414
|
+
**Sweep: ${SWEEP_ID}**
|
|
415
|
+
Released by /recover-orphans." >/dev/null 2>&1 || true
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
# resume_orphan_inline <issue> — run after claim_orphan succeeded. The subshell EXIT trap releases the claim always:
|
|
419
|
+
# on success and when the Skill call fails. The keepalive runs alongside the Skill call and is stopped by the release.
|
|
420
|
+
resume_orphan_inline() {
|
|
421
|
+
local num="$1"
|
|
422
|
+
(
|
|
423
|
+
trap 'release_orphan "$num"' EXIT
|
|
424
|
+
start_claim_keepalive "$num"
|
|
425
|
+
refresh_orphan "$num"
|
|
426
|
+
Skill(skill="{FORGE_SKILL_PREFIX}work-on", args="${num}")
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
310
431
|
```bash
|
|
311
432
|
RECOVERY_RESULTS=""
|
|
312
433
|
|
|
@@ -352,8 +473,31 @@ for NUM in $ORPHAN_LIST; do
|
|
|
352
473
|
if [ "$DRY_RUN" = "true" ]; then
|
|
353
474
|
echo " [DRY-RUN] Would: gh pr merge $PR_NUM --merge --auto"
|
|
354
475
|
else
|
|
355
|
-
|
|
356
|
-
|
|
476
|
+
# CI gate (MANDATORY before any autonomous merge): merge only when every check on the PR is
|
|
477
|
+
# green. Field test: PRs merged to staging with checks pending or red (#3165), because branch
|
|
478
|
+
# protection required none and an auto-merge waits only for *required* checks.
|
|
479
|
+
CI_GATE_SCRIPT=""
|
|
480
|
+
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
481
|
+
for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do
|
|
482
|
+
case "$_c" in /*) [ -z "$CI_GATE_SCRIPT" ] && [ -f "$_c/scripts/wait-ci-green.sh" ] && CI_GATE_SCRIPT="$_c/scripts/wait-ci-green.sh" ;; esac
|
|
483
|
+
done
|
|
484
|
+
if [ -n "$CI_GATE_SCRIPT" ]; then CI_GATE_OUT=$(bash "$CI_GATE_SCRIPT" "$PR_NUM" ${GH_FLAG}); CI_GATE_RC=$?
|
|
485
|
+
else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
|
|
486
|
+
echo "$CI_GATE_OUT"
|
|
487
|
+
GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
|
|
488
|
+
# rc 3 = CI still running: re-run this block (up to 3 more times) before treating it as a failure.
|
|
489
|
+
if [ "$CI_GATE_RC" -eq 0 ]; then
|
|
490
|
+
MERGE_RESULT=$(gh pr merge "$PR_NUM" ${GH_FLAG} --merge --auto --match-head-commit "$GATED_HEAD" 2>&1)
|
|
491
|
+
MERGE_EXIT=$?
|
|
492
|
+
else
|
|
493
|
+
MERGE_RESULT="not merged: CI gate rc=${CI_GATE_RC}"; MERGE_EXIT=1
|
|
494
|
+
CI_MSG="⛔ /recover-orphans did not merge PR #${PR_NUM}: CI is not green.
|
|
495
|
+
\`\`\`
|
|
496
|
+
${CI_GATE_OUT}
|
|
497
|
+
\`\`\`"
|
|
498
|
+
gh issue comment "$NUM" ${GH_FLAG} --body "$CI_MSG" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
499
|
+
gh issue edit "$NUM" ${GH_FLAG} --add-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
500
|
+
fi
|
|
357
501
|
echo " Merge result (exit $MERGE_EXIT): $MERGE_RESULT"
|
|
358
502
|
if [ $MERGE_EXIT -eq 0 ]; then
|
|
359
503
|
# Close the issue explicitly (Closes # only auto-closes on default branch)
|
|
@@ -373,25 +517,87 @@ for NUM in $ORPHAN_LIST; do
|
|
|
373
517
|
# Open PR awaiting review — invoke /review-pr
|
|
374
518
|
echo " Applying review-pr: invoking /review-pr on PR #$PR_NUM"
|
|
375
519
|
if [ "$DRY_RUN" = "true" ]; then
|
|
376
|
-
echo " [DRY-RUN] Would: Skill(skill='review-pr', args='$PR_NUM --auto-merge --issue $NUM --gh-flag $GH_FLAG')"
|
|
520
|
+
echo " [DRY-RUN] Would: Skill(skill='{FORGE_SKILL_PREFIX}review-pr', args='$PR_NUM --auto-merge --issue $NUM --gh-flag $GH_FLAG')"
|
|
377
521
|
else
|
|
378
|
-
|
|
379
|
-
#
|
|
380
|
-
|
|
381
|
-
|
|
522
|
+
# forge#3148: count trail refusals that predate this sweep, so a refusal from an earlier sweep escalates
|
|
523
|
+
# instead of re-reviewing the same unrepaired trail every run.
|
|
524
|
+
# forge#3159: count only TRUSTED comments (pipeline bot, or OWNER/MEMBER/COLLABORATOR) newer than the latest
|
|
525
|
+
# repair (the most recent needs-human unlabel event), so a stranger's comment or a refusal a human already
|
|
526
|
+
# repaired never forces an escalation. Epoch fallback on a failed lookup counts more, i.e. fails toward escalation.
|
|
527
|
+
# --paginate: the comments endpoint returns 30 per page, oldest first. An unreadable count fails toward escalation.
|
|
528
|
+
PRIOR_TRAIL_CUTOFF=$(gh api --paginate "repos/${GH_REPO}/issues/${NUM}/events" \
|
|
529
|
+
--jq '.[] | select(.event == "unlabeled" and .label.name == "needs-human") | .created_at' 2>/dev/null \
|
|
530
|
+
| sort | tail -1)
|
|
531
|
+
PRIOR_TRAIL_CUTOFF="${PRIOR_TRAIL_CUTOFF:-1970-01-01T00:00:00Z}"
|
|
532
|
+
# gh api --jq takes one expression and no --arg, so the cutoff goes through a real jq pipe.
|
|
533
|
+
if PRIOR_TRAIL_IDS=$(set -o pipefail; gh api --paginate "repos/${GH_REPO}/issues/${NUM}/comments" 2>/dev/null \
|
|
534
|
+
| jq -r --arg cutoff "$PRIOR_TRAIL_CUTOFF" '.[] | select(.body | contains("FORGE:PHASE_TRAIL_FAILED"))
|
|
535
|
+
| select(.created_at > $cutoff)
|
|
536
|
+
| select(.user.type == "Bot" or (.author_association | IN("OWNER","MEMBER","COLLABORATOR"))) | .id'); then
|
|
537
|
+
PRIOR_TRAIL_FAILS=$(printf '%s\n' "$PRIOR_TRAIL_IDS" | grep -c '[0-9]')
|
|
538
|
+
else
|
|
539
|
+
PRIOR_TRAIL_FAILS=1
|
|
540
|
+
fi
|
|
541
|
+
REVIEW_STATUS=""; REVIEW_BLOCKER="" # reset per orphan so a previous orphan's result never leaks into this one
|
|
542
|
+
Skill(skill="{FORGE_SKILL_PREFIX}review-pr", args="${PR_NUM} --auto-merge --issue ${NUM} --gh-flag ${GH_FLAG}")
|
|
543
|
+
# REVIEW_STATUS = the `status:` field of the REVIEW_RESULT block the Skill call returned.
|
|
544
|
+
if [ "$REVIEW_STATUS" = "PHASE_TRAIL_FAILED" ]; then
|
|
545
|
+
# Phase 8 refused the merge: the issue's phase trail is incomplete (review-pr posted the
|
|
546
|
+
# MISSING lines as FORGE:PHASE_TRAIL_FAILED). Do NOT set workflow:in-review — that only re-queues
|
|
547
|
+
# the same refusal for the next sweep. Never hand-post the missing markers.
|
|
548
|
+
if [ "${PRIOR_TRAIL_FAILS:-0}" -gt 0 ]; then
|
|
549
|
+
gh issue edit "$NUM" ${GH_FLAG} --add-label "needs-human" 2>/dev/null || true
|
|
550
|
+
gh issue comment "$NUM" ${GH_FLAG} --body "<!-- FORGE:ORPHAN_RECOVERED -->
|
|
551
|
+
## Orphan Recovery — Escalated
|
|
552
|
+
|
|
553
|
+
**Action**: \`/review-pr\` refused to merge PR #${PR_NUM} again because the phase trail is incomplete, and an earlier refusal was not repaired.
|
|
554
|
+
**Recovered by**: /recover-orphans
|
|
555
|
+
|
|
556
|
+
See the latest FORGE:PHASE_TRAIL_FAILED comment for the missing phases. Re-run each one via its Skill (or \`/work-on ${NUM}\`), then remove \`needs-human\`." 2>/dev/null || true
|
|
557
|
+
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} refused (phase trail) again — needs-human added |\n"
|
|
558
|
+
else
|
|
559
|
+
# First refusal: resume /work-on, whose resume preflight re-runs each missing phase via Skill(...)
|
|
560
|
+
# and then re-enters review (work-on.md Phase 0B resume → work-on/review.md).
|
|
561
|
+
if claim_orphan "$NUM"; then
|
|
562
|
+
resume_orphan_inline "$NUM" # releases the claim on every exit
|
|
563
|
+
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} refused (phase trail) — resumed /work-on to re-run missing phases |\n"
|
|
564
|
+
else
|
|
565
|
+
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} refused (phase trail) — /work-on resume skipped: ${CLAIM_SKIP_REASON} |\n"
|
|
566
|
+
fi
|
|
567
|
+
fi
|
|
568
|
+
elif [ "$REVIEW_STATUS" = "BLOCKED" ] && echo "$REVIEW_BLOCKER" | grep -qE 'phase trail|auto-merge requires --issue'; then
|
|
569
|
+
# REVIEW_BLOCKER = the `blocker:` field of the same REVIEW_RESULT block. The gate could not run (verifier
|
|
570
|
+
# rc>=2 / unresolvable) or had no issue to verify (forge#3147). review-pr already added needs-human for the
|
|
571
|
+
# rc>=2 case; add it here too so the diagnosis skip above stops re-sweeping this orphan.
|
|
572
|
+
gh issue edit "$NUM" ${GH_FLAG} --add-label "needs-human" 2>/dev/null || true
|
|
573
|
+
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} blocked by the merge gate — needs-human added |\n"
|
|
574
|
+
elif case "$REVIEW_STATUS" in COMPLETE|ALREADY_MERGED|BLOCKED) false ;; *) true ;; esac; then
|
|
575
|
+
# forge#3159: an empty or unrecognized status means the review result could not be read. Do not report
|
|
576
|
+
# "submitted for review" or re-queue it; escalate to a human.
|
|
577
|
+
gh issue edit "$NUM" ${GH_FLAG} --add-label "needs-human" 2>/dev/null || true
|
|
578
|
+
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} unrecognized review status '${REVIEW_STATUS}' — needs-human added |\n"
|
|
579
|
+
else
|
|
580
|
+
# After review: update label
|
|
581
|
+
gh issue edit "$NUM" ${GH_FLAG} --add-label "workflow:in-review" \
|
|
582
|
+
--remove-label "workflow:building,workflow:awaiting-merge" 2>/dev/null || true
|
|
583
|
+
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} submitted for review |\n"
|
|
584
|
+
fi
|
|
382
585
|
fi
|
|
383
|
-
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} submitted for review |\n"
|
|
586
|
+
[ "$DRY_RUN" = "true" ] && RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | review-pr | PR #${PR_NUM} would be submitted for review |\n"
|
|
384
587
|
;;
|
|
385
588
|
|
|
386
589
|
create-pr)
|
|
387
590
|
# Branch has commits but no PR — resume /work-on to create PR
|
|
388
591
|
echo " Applying create-pr: resuming /work-on to advance from build to PR creation"
|
|
389
592
|
if [ "$DRY_RUN" = "true" ]; then
|
|
390
|
-
echo " [DRY-RUN] Would: Skill(skill='work-on', args='$NUM')"
|
|
593
|
+
echo " [DRY-RUN] Would: Skill(skill='{FORGE_SKILL_PREFIX}work-on', args='$NUM')"
|
|
594
|
+
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | Would resume /work-on — branch $BRANCH has commits, no PR |\n"
|
|
595
|
+
elif claim_orphan "$NUM"; then
|
|
596
|
+
resume_orphan_inline "$NUM" # releases the claim on every exit
|
|
597
|
+
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | Resumed /work-on — branch $BRANCH has commits, no PR |\n"
|
|
391
598
|
else
|
|
392
|
-
|
|
599
|
+
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | /work-on resume skipped: ${CLAIM_SKIP_REASON} |\n"
|
|
393
600
|
fi
|
|
394
|
-
RECOVERY_RESULTS="${RECOVERY_RESULTS}| #${NUM} | create-pr | Resumed /work-on — branch $BRANCH has commits, no PR |\n"
|
|
395
601
|
;;
|
|
396
602
|
|
|
397
603
|
reset-labels)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Review agent catalog (read by /review-pr; not a user entrypoint)
|
|
3
|
+
user-invocable: false
|
|
4
|
+
---
|
|
1
5
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
2
6
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
3
7
|
|
|
@@ -37,7 +41,7 @@ For each agent selected in Phase 3B, read **only** the files relevant to that ag
|
|
|
37
41
|
|
|
38
42
|
1. **Always read first** (shared protocols, all agents require this):
|
|
39
43
|
```
|
|
40
|
-
Read: $
|
|
44
|
+
Read: ${CLAUDE_PLUGIN_ROOT}/commands/review-pr-agents/protocols.md
|
|
41
45
|
```
|
|
42
46
|
|
|
43
47
|
2. **Then read the persona file for each selected agent**:
|
|
@@ -70,11 +74,11 @@ For each agent selected in Phase 3B, read **only** the files relevant to that ag
|
|
|
70
74
|
|
|
71
75
|
## Loading Instructions for Phase 3C
|
|
72
76
|
|
|
73
|
-
Replace the single `Read: $
|
|
77
|
+
Replace the single `Read: ${CLAUDE_PLUGIN_ROOT}/commands/review-pr-agents.md` call with:
|
|
74
78
|
|
|
75
79
|
```
|
|
76
|
-
Read: $
|
|
77
|
-
Read: $
|
|
80
|
+
Read: ${CLAUDE_PLUGIN_ROOT}/commands/review-pr-agents/protocols.md
|
|
81
|
+
Read: ${CLAUDE_PLUGIN_ROOT}/commands/review-pr-agents/<persona>.md (one per selected agent)
|
|
78
82
|
```
|
|
79
83
|
|
|
80
84
|
Each persona file contains:
|
|
@@ -87,3 +91,5 @@ The `protocols.md` file contains:
|
|
|
87
91
|
- Tool-Result Truncation Discipline
|
|
88
92
|
- Evidence-Based Review Protocol (all agents follow)
|
|
89
93
|
- Structured Findings Protocol (machine-readable findings block format)
|
|
94
|
+
|
|
95
|
+
**Spec root (MANDATORY)**: read every file named in this catalog from `${CLAUDE_PLUGIN_ROOT}` — the install root of the ForgeDock plugin that is running this command (Claude Code fills it in when it loads the spec). Only if that path does not start with `/` (not a Claude Code plugin session: install.sh, Codex, OpenCode) use `$FORGE_HOME` instead. Never read sub-files from `$FORGE_HOME` when the plugin root resolved: an exported `FORGE_HOME` can point at a different, older ForgeDock checkout, and mixing roots runs stale phase specs.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
user-invocable: false
|
|
2
3
|
description: Staging review mode — comprehensive review of staging branch before deploy to main
|
|
3
4
|
argument-hint: "[PR number or \"staging\"]"
|
|
4
5
|
allowed-tools: Task, Agent, Bash, Read, Grep, Glob, WebFetch
|
|
@@ -9,7 +10,9 @@ install: core
|
|
|
9
10
|
|
|
10
11
|
# Staging Review
|
|
11
12
|
|
|
12
|
-
**
|
|
13
|
+
> **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
|
|
14
|
+
|
|
15
|
+
**Trigger**: Invoked by the orchestrator via `Skill("{FORGE_SKILL_PREFIX}review-pr-staging", $ARGUMENTS)`, or directly with `$ARGUMENTS` = "staging", a PR number targeting main, or "staging:feature".
|
|
13
16
|
|
|
14
17
|
Performs comprehensive review of `staging` before merging to `main`. Handles large diffs (1,000-10,000+ lines), diverse changes, deep analysis, and business impact assessment.
|
|
15
18
|
|
|
@@ -502,7 +505,13 @@ Missing persona templates are a fatal setup error, not permission to skip multi-
|
|
|
502
505
|
|
|
503
506
|
```bash
|
|
504
507
|
TEMPLATE_BASE=""
|
|
505
|
-
|
|
508
|
+
# Tier 0: the running plugin's own root (Claude Code substitutes this exact spelling; elsewhere it stays a
|
|
509
|
+
# literal that the /* check rejects). Wins over an exported FORGE_HOME, which may name an older checkout.
|
|
510
|
+
_PLUGIN_ROOT='${CLAUDE_PLUGIN_ROOT}'; case "$_PLUGIN_ROOT" in /*) ;; *) _PLUGIN_ROOT="" ;; esac
|
|
511
|
+
if [[ -n "$_PLUGIN_ROOT" && -f "$_PLUGIN_ROOT/commands/review-pr-agents/protocols.md" ]]; then
|
|
512
|
+
TEMPLATE_BASE="$_PLUGIN_ROOT/commands/review-pr-agents"
|
|
513
|
+
TEMPLATE_SOURCE="plugin_root"
|
|
514
|
+
elif [[ -f "$FORGE_HOME/commands/review-pr-agents/protocols.md" ]]; then
|
|
506
515
|
TEMPLATE_BASE="$FORGE_HOME/commands/review-pr-agents"
|
|
507
516
|
TEMPLATE_SOURCE="forge_home"
|
|
508
517
|
else
|
|
@@ -530,7 +539,7 @@ fi
|
|
|
530
539
|
|
|
531
540
|
**If `TEMPLATE_SOURCE` is `none`**: HARD STOP. Post a PR comment explaining the setup is broken, instructing the user to run `npx forgedock update` to repair the install, add `needs-human`, and exit without posting any findings or a verdict. **NEVER perform the review inline in the main agent context as a substitute.**
|
|
532
541
|
|
|
533
|
-
**If `TEMPLATE_SOURCE` is `forge_home` or `repo_path`** (normal cases — behavior unchanged): `Read: $TEMPLATE_BASE/protocols.md` and `Read: $TEMPLATE_BASE/<persona>.md` per selected agent.
|
|
542
|
+
**If `TEMPLATE_SOURCE` is `plugin_root`, `forge_home` or `repo_path`** (normal cases — behavior unchanged): `Read: $TEMPLATE_BASE/protocols.md` and `Read: $TEMPLATE_BASE/<persona>.md` per selected agent.
|
|
534
543
|
|
|
535
544
|
**If `TEMPLATE_SOURCE` is `monolithic_catalog`** (last resort): `Read: $MONOLITHIC_CATALOG` and extract the shared protocols section plus each selected persona's section from within that single file.
|
|
536
545
|
|
|
@@ -593,7 +602,7 @@ TEST_GATE_REASON="Phase 6.5 not yet run"
|
|
|
593
602
|
|
|
594
603
|
# Invoke /test-gate with the bundle PRs already computed in Phase 0A
|
|
595
604
|
# ALL_PR_NUMBERS is the de-duplicated union of both scan methods (commit log + merge subjects)
|
|
596
|
-
GATE_OUTPUT=$(Skill("test-gate", "--prs \"$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs)\" --base $DEFAULT_BRANCH"))
|
|
605
|
+
GATE_OUTPUT=$(Skill("{FORGE_SKILL_PREFIX}test-gate", "--prs \"$(echo $ALL_PR_NUMBERS | tr '\n' ' ' | xargs)\" --base $DEFAULT_BRANCH"))
|
|
597
606
|
|
|
598
607
|
# Extract machine-readable verdict from Skill output
|
|
599
608
|
TEST_GATE_VERDICT=$(echo "$GATE_OUTPUT" | grep -oP '(?<=FORGE:TEST_GATE:RESULT=)(BLOCK|PASS|SKIP)' | tail -1 || echo "SKIP")
|
|
@@ -872,7 +881,7 @@ else
|
|
|
872
881
|
# --label is repeatable (not comma-joined) per the /issue programmatic contract.
|
|
873
882
|
# ${MILESTONE_FLAG} carries the Phase 7D derivation through — empty string is
|
|
874
883
|
# a no-op arg when the reviewed branch has no milestone (plain staging→main).
|
|
875
|
-
ISSUE_SKILL_OUTPUT=$(Skill(skill="issue", args="--title \"$STAGING_FINDING_TITLE\" --body-file \"$STAGING_FINDING_BODY_FILE\" --label review-finding --label needs-validation --label staging-review --label \"$STAGING_FINDING_PRIORITY\" ${MILESTONE_FLAG}"))
|
|
884
|
+
ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"$STAGING_FINDING_TITLE\" --body-file \"$STAGING_FINDING_BODY_FILE\" --label review-finding --label needs-validation --label staging-review --label \"$STAGING_FINDING_PRIORITY\" ${MILESTONE_FLAG}"))
|
|
876
885
|
# /issue re-reads the created issue and hard-fails unless this exact marker is present.
|
|
877
886
|
rm -f "$STAGING_FINDING_BODY_FILE"
|
|
878
887
|
|