forgedock 1.8.0 → 1.8.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 (67) hide show
  1. package/bin/engine/admission.mjs +57 -1
  2. package/bin/engine/admission.test.mjs +60 -0
  3. package/commands/analytics.md +3 -1
  4. package/commands/audit-agents.md +3 -1
  5. package/commands/audit.md +10 -8
  6. package/commands/autopilot.md +16 -14
  7. package/commands/ci-audit.md +4 -2
  8. package/commands/cleanup.md +1 -1
  9. package/commands/deploy-pr.md +31 -11
  10. package/commands/geo-audit.md +3 -1
  11. package/commands/incident-response.md +3 -1
  12. package/commands/issue.md +4 -2
  13. package/commands/milestone.md +8 -6
  14. package/commands/orchestrate/config.md +2 -2
  15. package/commands/orchestrate/phase-1-resolve.md +56 -12
  16. package/commands/orchestrate/phase-3-dependency.md +15 -5
  17. package/commands/orchestrate/phase-4-execution.md +593 -78
  18. package/commands/orchestrate/phase-5-cleanup.md +4 -2
  19. package/commands/orchestrate/phase-6-report.md +7 -3
  20. package/commands/orchestrate.md +13 -9
  21. package/commands/pipeline-health/phase-4-proposals.md +6 -4
  22. package/commands/pipeline-health/phase-5-report.md +4 -2
  23. package/commands/pipeline-health.md +9 -7
  24. package/commands/pipeline-resume.md +4 -2
  25. package/commands/qa-sweep.md +3 -1
  26. package/commands/quality-gate.md +4 -3
  27. package/commands/recover-orphans.md +172 -13
  28. package/commands/review-pr-agents.md +6 -4
  29. package/commands/review-pr-staging.md +13 -5
  30. package/commands/review-pr.md +187 -19
  31. package/commands/rollback.md +3 -1
  32. package/commands/security-audit.md +3 -1
  33. package/commands/signal-planner.md +6 -4
  34. package/commands/sync-ecosystem.md +4 -2
  35. package/commands/test-gate.md +4 -2
  36. package/commands/upgrade-deps.md +4 -2
  37. package/commands/validate.md +3 -1
  38. package/commands/work-on/build/architect.md +81 -19
  39. package/commands/work-on/build/context.md +61 -18
  40. package/commands/work-on/build/implement.md +62 -18
  41. package/commands/work-on/build/validate.md +240 -60
  42. package/commands/work-on/build.md +344 -146
  43. package/commands/work-on/close.md +617 -246
  44. package/commands/work-on/decompose.md +239 -41
  45. package/commands/work-on/investigate.md +377 -61
  46. package/commands/work-on/remediate.md +42 -9
  47. package/commands/work-on/review.md +240 -29
  48. package/commands/work-on-monolithic.md +48 -7
  49. package/commands/work-on.md +180 -2014
  50. package/docs/CODEX.md +2 -0
  51. package/docs/CONFIG.md +11 -0
  52. package/package.json +1 -1
  53. package/scripts/build-spec-graph.mjs +11 -4
  54. package/scripts/check-spec-markers.sh +11 -1
  55. package/scripts/checkbox-sections.test.sh +8 -23
  56. package/scripts/extract-affected-files.sh +5 -2
  57. package/scripts/extract-affected-files.test.sh +27 -0
  58. package/scripts/forge-root.test.sh +176 -0
  59. package/scripts/is-docs-only.sh +31 -0
  60. package/scripts/is-docs-only.test.sh +64 -0
  61. package/scripts/lint-dispatch-prompt.sh +217 -0
  62. package/scripts/lint-dispatch-prompt.test.sh +307 -0
  63. package/scripts/validate-spec-graph.sh +3 -2
  64. package/scripts/verify-phase-trail.sh +282 -0
  65. package/scripts/verify-phase-trail.test.sh +298 -0
  66. package/scripts/wait-ci-green.sh +98 -0
  67. package/scripts/wait-ci-green.test.sh +99 -0
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # wait-ci-green.sh — CI gate every autonomous merge must pass first.
6
+ #
7
+ # Usage: wait-ci-green.sh <pr-number> -R <owner/repo> [--timeout SECONDS] [--interval SECONDS]
8
+ #
9
+ # Waits until every check on the PR has finished, then decides. Fails closed: a merge may
10
+ # proceed ONLY on exit 0. Field test: pipeline agents merged PRs to staging while checks were
11
+ # pending or red (#3165 merged with the side-effect gate failing), because neither the specs nor
12
+ # branch protection required green CI, and `gh pr merge --auto` only waits for *required* checks.
13
+ #
14
+ # Exit codes (one "CI_GATE: <RESULT>" line on stdout, details after it):
15
+ # 0 PASS every check is pass/skipping (or the repo reports no checks after the grace period
16
+ # and FORGE_CI_REQUIRE_CHECKS is not 1)
17
+ # 1 FAIL at least one check failed or was cancelled (listed)
18
+ # 2 ERROR bad usage, gh unreadable for too long, the PR head moved during the wait, or
19
+ # FORGE_CI_REQUIRE_CHECKS=1 and no checks were reported
20
+ # 3 TIMEOUT checks still pending when the timeout expired (listed)
21
+ #
22
+ # Every result also prints "CI_GATE_HEAD: <sha>" — the exact PR head the decision applies to. Callers
23
+ # MUST merge with `--match-head-commit <that sha>` so a push after the gate cannot land unchecked.
24
+ #
25
+ # Env: FORGE_CI_TIMEOUT (default 540 — stays under the 10-minute Bash tool limit; on TIMEOUT the
26
+ # caller re-runs the gate, it does not raise the timeout), FORGE_CI_INTERVAL (default 20),
27
+ # FORGE_CI_NO_CHECKS_GRACE (default 120), FORGE_CI_SETTLE (default 45 — never PASS earlier than
28
+ # this after the wait starts, so workflows that register late are not missed),
29
+ # FORGE_CI_REQUIRE_CHECKS (default 0).
30
+ # "No checks" passes only when GitHub Actions also reports no workflow runs for the head commit;
31
+ # if runs exist but their checks have not appeared yet, the gate keeps waiting.
32
+ set -uo pipefail
33
+
34
+ PR=""; REPO=""
35
+ TIMEOUT="${FORGE_CI_TIMEOUT:-540}"; INTERVAL="${FORGE_CI_INTERVAL:-20}"; SETTLE="${FORGE_CI_SETTLE:-45}"
36
+ GRACE="${FORGE_CI_NO_CHECKS_GRACE:-120}"; REQUIRE="${FORGE_CI_REQUIRE_CHECKS:-0}"
37
+ usage() { echo "CI_GATE: ERROR"; echo "CI_GATE_HEAD: unknown"; echo "usage: wait-ci-green.sh <pr> -R <owner/repo> [--timeout S] [--interval S]" >&2; exit 2; }
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ -R) [ $# -ge 2 ] || usage; REPO="$2"; shift 2 ;;
41
+ -R*) REPO="${1#-R}"; REPO="${REPO# }"; shift ;;
42
+ --timeout) [ $# -ge 2 ] || usage; TIMEOUT="$2"; shift 2 ;;
43
+ --interval) [ $# -ge 2 ] || usage; INTERVAL="$2"; shift 2 ;;
44
+ -h|--help) sed -n '5,25p' "$0"; exit 0 ;;
45
+ *) [ -z "$PR" ] || usage; PR="$1"; shift ;;
46
+ esac
47
+ done
48
+ case "$PR" in ''|*[!0-9]*) usage ;; esac
49
+ case "$REPO" in */*) ;; *) usage ;; esac
50
+ for v in "$TIMEOUT" "$INTERVAL" "$GRACE" "$SETTLE"; do case "$v" in ''|*[!0-9]*) usage ;; esac; done
51
+ [ "$INTERVAL" -ge 1 ] || INTERVAL=1
52
+
53
+ emit() { echo "CI_GATE: $1"; echo "CI_GATE_HEAD: ${START_SHA:-unknown}"; }
54
+ head_sha() { gh pr view "$PR" -R "$REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null; }
55
+ START_SHA="$(head_sha)"
56
+ if [ -z "$START_SHA" ]; then START_SHA=""; emit ERROR; echo "cannot read PR #$PR head in $REPO"; exit 2; fi
57
+
58
+ start=$(date +%s); errors=0
59
+ while :; do
60
+ now=$(date +%s); elapsed=$((now - start))
61
+ if ! JSON="$(gh pr checks "$PR" -R "$REPO" --json name,bucket 2>/dev/null)" || ! printf '%s' "$JSON" | jq -e 'type=="array"' >/dev/null 2>&1; then
62
+ # gh pr checks exits non-zero both on outage and on "no checks reported"; tell them apart by output.
63
+ if printf '%s' "${JSON:-}" | jq -e 'type=="array"' >/dev/null 2>&1; then :; else
64
+ # Capture first: under pipefail, `gh ... | grep -q` fails whenever gh exits 1, which it
65
+ # does precisely in the "no checks reported" case this branch exists to detect.
66
+ NC_OUT="$(gh pr checks "$PR" -R "$REPO" 2>&1 || true)"
67
+ if printf '%s' "$NC_OUT" | grep -qi 'no checks reported'; then JSON='[]'
68
+ else
69
+ errors=$((errors + 1))
70
+ if [ "$errors" -ge 5 ]; then emit ERROR; echo "gh pr checks unreadable 5 times in a row"; exit 2; fi
71
+ sleep "$INTERVAL"; continue
72
+ fi
73
+ fi
74
+ fi
75
+ errors=0
76
+ cur="$(head_sha)"
77
+ if [ -n "$cur" ] && [ "$cur" != "$START_SHA" ]; then emit ERROR; echo "PR head moved during the wait ($START_SHA -> $cur); re-review the new head"; exit 2; fi
78
+ total=$(printf '%s' "$JSON" | jq 'length')
79
+ failed=$(printf '%s' "$JSON" | jq -r '.[] | select(.bucket=="fail" or .bucket=="cancel") | " - \(.name) (\(.bucket))"')
80
+ pending=$(printf '%s' "$JSON" | jq -r '.[] | select(.bucket!="pass" and .bucket!="skipping" and .bucket!="fail" and .bucket!="cancel") | " - \(.name) (\(.bucket))"')
81
+ if [ -n "$failed" ]; then emit FAIL; echo "PR #$PR head ${START_SHA:0:7}: failing checks:"; echo "$failed"; exit 1; fi
82
+ if [ "$total" -eq 0 ]; then
83
+ if [ "$elapsed" -ge "$GRACE" ]; then
84
+ RUNS=$(gh api "repos/$REPO/actions/runs?head_sha=$START_SHA&per_page=1" --jq '.total_count' 2>/dev/null || echo "")
85
+ if [ -n "$RUNS" ] && [ "$RUNS" != "0" ]; then
86
+ : # workflow runs exist for this head but no checks are reported yet — keep waiting
87
+ elif [ "$REQUIRE" = "1" ]; then
88
+ emit ERROR; echo "no checks reported after ${GRACE}s and FORGE_CI_REQUIRE_CHECKS=1"; exit 2
89
+ else
90
+ emit PASS; echo "no checks reported for PR #$PR after ${GRACE}s and no workflow runs for its head (no CI on this branch)"; exit 0
91
+ fi
92
+ fi
93
+ elif [ -z "$pending" ] && [ "$elapsed" -ge "$SETTLE" ]; then
94
+ emit PASS; echo "PR #$PR head ${START_SHA:0:7}: $total checks passed or skipped"; exit 0
95
+ fi
96
+ if [ "$elapsed" -ge "$TIMEOUT" ]; then emit TIMEOUT; echo "PR #$PR: still pending after ${TIMEOUT}s:"; echo "$pending"; exit 3; fi
97
+ sleep "$INTERVAL"
98
+ done
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ # wait-ci-green.test.sh — offline tests for scripts/wait-ci-green.sh (fake gh on PATH).
5
+ # Usage: bash scripts/wait-ci-green.test.sh
6
+ set -uo pipefail
7
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; GATE="$HERE/wait-ci-green.sh"
8
+ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
9
+ PASS=0; FAILN=0
10
+ expect() { [ "$2" = "$3" ] && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: $1 (got '$3' want '$2')"; }; }
11
+
12
+ # Fake gh: `pr view --json headRefOid` prints line N of $T/sha (N = call count, last line repeats);
13
+ # `pr checks --json` prints the N-th fixture $T/checks.N (last one repeats); "ERR" fixture = outage.
14
+ mkdir -p "$T/bin"
15
+ cat > "$T/bin/gh" <<'GH'
16
+ #!/usr/bin/env bash
17
+ D="$FAKE_DIR"
18
+ if [ "$1 $2" = "pr view" ]; then
19
+ n=$(( $(cat "$D/sha.n" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$D/sha.n"
20
+ total=$(wc -l < "$D/sha"); [ "$n" -gt "$total" ] && n=$total; sed -n "${n}p" "$D/sha"; exit 0
21
+ fi
22
+ if [ "$1" = "api" ]; then
23
+ case "$2" in *actions/runs*) cat "$D/runs" 2>/dev/null || echo 0; exit 0 ;; esac
24
+ exit 1
25
+ fi
26
+ if [ "$1 $2" = "pr checks" ]; then
27
+ case " $* " in *" --json "*) ;; *) [ -f "$D/nochecks" ] && { echo "no checks reported on the 'x' branch" >&2; exit 1; }; exit 1 ;; esac
28
+ n=$(( $(cat "$D/chk.n" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$D/chk.n"
29
+ f="$D/checks.$n"; while [ ! -f "$f" ] && [ "$n" -gt 1 ]; do n=$((n-1)); f="$D/checks.$n"; done
30
+ c=$(cat "$f"); [ "$c" = "ERR" ] && { echo "HTTP 502" >&2; exit 1; }
31
+ [ "$c" = "NONE" ] && { echo "no checks reported on the 'x' branch" >&2; exit 1; }
32
+ echo "$c"; exit 0
33
+ fi
34
+ exit 1
35
+ GH
36
+ chmod +x "$T/bin/gh"
37
+
38
+ run() { # run <case-dir> [gate args...] -> prints "rc|first line"
39
+ local d="$1"; shift
40
+ local out; out=$(FAKE_DIR="$d" PATH="$T/bin:$PATH" FORGE_CI_INTERVAL=1 FORGE_CI_NO_CHECKS_GRACE=1 FORGE_CI_SETTLE="${SETTLE:-0}" bash "$GATE" 7 -R o/r "$@" 2>/dev/null); local rc=$?
41
+ printf '%s|%s' "$rc" "$(printf '%s\n' "$out" | head -1)"
42
+ }
43
+ mk() { local d="$T/$1"; mkdir -p "$d"; printf 'abc1234\n' > "$d/sha"; shift; local i=1; for c in "$@"; do printf '%s' "$c" > "$d/checks.$i"; i=$((i+1)); done; echo "$d"; }
44
+
45
+ P='[{"name":"a","bucket":"pass"},{"name":"b","bucket":"skipping"}]'
46
+ expect "all pass/skipping" "0|CI_GATE: PASS" "$(run "$(mk pass "$P")")"
47
+ expect "one failure" "1|CI_GATE: FAIL" "$(run "$(mk fail '[{"name":"a","bucket":"pass"},{"name":"b","bucket":"fail"}]')")"
48
+ expect "cancelled counts as failure" "1|CI_GATE: FAIL" "$(run "$(mk cancel '[{"name":"a","bucket":"cancel"}]')")"
49
+ expect "pending then pass" "0|CI_GATE: PASS" "$(run "$(mk pend '[{"name":"a","bucket":"pending"}]' "$P")")"
50
+ expect "pending then fail" "1|CI_GATE: FAIL" "$(run "$(mk pendfail '[{"name":"a","bucket":"pending"}]' '[{"name":"a","bucket":"fail"}]')")"
51
+ expect "pending past timeout" "3|CI_GATE: TIMEOUT" "$(run "$(mk slow '[{"name":"a","bucket":"pending"}]')" --timeout 2)"
52
+ expect "fail wins over pending" "1|CI_GATE: FAIL" "$(run "$(mk mixed '[{"name":"a","bucket":"pending"},{"name":"b","bucket":"fail"}]')")"
53
+ expect "transient outage then pass" "0|CI_GATE: PASS" "$(run "$(mk blip ERR "$P")")"
54
+ expect "persistent outage" "2|CI_GATE: ERROR" "$(run "$(mk down ERR)" --timeout 60)"
55
+ d=$(mk none NONE); touch "$d/nochecks"
56
+ expect "no checks after grace passes" "0|CI_GATE: PASS" "$(run "$d")"
57
+ expect "no checks + REQUIRE fails closed" "2|CI_GATE: ERROR" "$(FORGE_CI_REQUIRE_CHECKS=1 run "$d")"
58
+ d=$(mk moved '[{"name":"a","bucket":"pending"}]' "$P"); printf 'abc1234\nabc1234\ndef5678\n' > "$d/sha"
59
+ expect "head moved during wait" "2|CI_GATE: ERROR" "$(run "$d")"
60
+ expect "bad pr number" "2|CI_GATE: ERROR" "$(FAKE_DIR="$T/pass" PATH="$T/bin:$PATH" bash "$GATE" abc -R o/r 2>/dev/null | head -1 | sed 's/^/2|/')"
61
+ expect "missing repo" "2" "$(FAKE_DIR="$T/pass" PATH="$T/bin:$PATH" bash "$GATE" 7 >/dev/null 2>&1; echo $?)"
62
+
63
+ d=$(mk runsnochecks NONE); touch "$d/nochecks"; echo 3 > "$d/runs"
64
+ expect "no checks but workflow runs exist keeps waiting (timeout)" "3|CI_GATE: TIMEOUT" "$(run "$d" --timeout 3)"
65
+ expect "pass is held until the settle period" "0|CI_GATE: PASS" "$(SETTLE=2 run "$(mk settle "$P")")"
66
+ out=$(FAKE_DIR="$(mk headline "$P")" PATH="$T/bin:$PATH" FORGE_CI_INTERVAL=1 FORGE_CI_SETTLE=0 bash "$GATE" 7 -R o/r 2>/dev/null)
67
+ expect "every result names the gated head" "CI_GATE_HEAD: abc1234" "$(printf '%s\n' "$out" | grep '^CI_GATE_HEAD:')"
68
+
69
+ # Static guard: every autonomous `gh pr merge` command line in the pipeline specs must be preceded
70
+ # (within 25 lines) by a CI gate result check. Prose/manual instructions are not command lines.
71
+ for spec in commands/review-pr.md commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
72
+ bad=$(awk '
73
+ { line[NR]=$0 }
74
+ /^[[:space:]]*(MERGE_RESULT=\$\()?gh pr merge / {
75
+ ok=0; for (i=NR-1; i>0 && i>=NR-25; i--) if (line[i] ~ /CI_GATE_RC/) { ok=1; break }
76
+ if (!ok) print FILENAME ":" NR
77
+ }' "$HERE/../$spec")
78
+ expect "every autonomous merge in $spec is CI-gated" "" "$bad"
79
+ done
80
+ # Every autonomous merge pins the gated head (forge#3187): no merge line may omit --match-head-commit.
81
+ for spec in commands/review-pr.md commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
82
+ unpinned=$(grep -nE '^[[:space:]]*(MERGE_RESULT=\$\(|MERGE_OUTPUT=\$\()?gh pr merge ' "$HERE/../$spec" | grep -v -- '--match-head-commit')
83
+ expect "every autonomous merge in $spec pins the gated head" "" "$unpinned"
84
+ done
85
+ # The gate's own default timeout stays under the 10-minute Bash tool limit (forge#3189).
86
+ expect "default gate timeout fits one Bash call" "540" "$(sed -n 's/.*TIMEOUT="\${FORGE_CI_TIMEOUT:-\([0-9]*\)}".*/\1/p' "$GATE")"
87
+
88
+ # The /work-on router never merges: every merge lives in a CI-gated phase.
89
+ if grep -qE '^[[:space:]]*(MERGE_RESULT=\$\()?gh pr merge ' "$HERE/../commands/work-on.md"; then FAILN=$((FAILN+1)); echo "FAIL: work-on.md router contains a merge command"; else PASS=$((PASS+1)); fi
90
+
91
+ # Reviewed-head guard: review-pr Phase 8 must refuse to merge a head other than the reviewed commit,
92
+ # and the work-on callers must route "stale review" to a re-review, never a manual merge.
93
+ grep -qF '${GATED_HEAD#"$REVIEWED_SHORT"}' "$HERE/../commands/review-pr.md" && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: review-pr Phase 8 lacks reviewed-head guard"; }
94
+ for f in commands/work-on/review.md; do
95
+ grep -q 'stale review' "$HERE/../$f" && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: $f does not route stale review"; }
96
+ done
97
+
98
+ echo "wait-ci-green tests: pass=$PASS fail=$FAILN"
99
+ [ "$FAILN" -eq 0 ]