forgedock 1.18.0 → 1.19.0
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/outcome-codes.mjs +13 -0
- package/bin/engine/phases.mjs +54 -4
- package/bin/engine/runlog.mjs +10 -2
- package/bin/engine/telemetry-seam.mjs +3 -0
- package/bin/engine/telemetry.test.mjs +3 -2
- package/bin/engine-cli.mjs +32 -2
- package/bin/engine.mjs +139 -7
- package/bin/runner.mjs +69 -9
- package/bin/stats/aggregate.mjs +7 -3
- package/bin/stats/cache.mjs +7 -3
- package/bin/stats/github-source.mjs +136 -28
- package/bin/stats/index.mjs +46 -9
- package/bin/stats/render.mjs +1 -1
- package/bin/stats/stats.test.mjs +201 -40
- package/commands/review-pr-staging.md +4 -4
- package/commands/review-pr.md +25 -15
- package/commands/work-on/build/architect.md +53 -0
- package/commands/work-on/build/implement.md +19 -0
- package/commands/work-on/build/validate.md +320 -28
- package/commands/work-on/build.md +66 -9
- package/commands/work-on/investigate.md +7 -0
- package/commands/work-on/remediate.md +32 -13
- package/commands/work-on/review.md +32 -19
- package/docs/CONFIG.md +89 -2
- package/docs/OUTCOME-CODES.md +5 -1
- package/docs/STATS.md +11 -4
- package/docs/TELEMETRY.md +5 -2
- package/docs/WORK-ON-RUNTIME.md +18 -0
- package/package.json +2 -2
- package/packages/protocol/src/types.js +1 -1
- package/scripts/acceptance-red-green.sh +90 -0
- package/scripts/acceptance-red-green.test.sh +74 -0
- package/scripts/cap-findings.test.sh +2 -0
- package/scripts/check-contract-scope.test.sh +2 -0
- package/scripts/check-edge-cases.sh +143 -0
- package/scripts/check-edge-cases.test.sh +175 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/check-test-tmp-cleanup.sh +56 -0
- package/scripts/check-test-tmp-cleanup.test.sh +69 -0
- package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
- package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
- package/scripts/ci-local/adapters/buildkite.mjs +58 -0
- package/scripts/ci-local/adapters/circleci.mjs +101 -0
- package/scripts/ci-local/adapters/common.mjs +115 -0
- package/scripts/ci-local/adapters/explicit.mjs +21 -0
- package/scripts/ci-local/adapters/github-actions.mjs +349 -0
- package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
- package/scripts/ci-local/adapters/portability.mjs +104 -0
- package/scripts/ci-local/adapters/task-runners.mjs +67 -0
- package/scripts/ci-local/cli.mjs +50 -0
- package/scripts/ci-local/lib.mjs +371 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +345 -0
- package/scripts/ci-local/scoping.mjs +508 -0
- package/scripts/ci-local.sh +97 -0
- package/scripts/ci-local.test.sh +919 -0
- package/scripts/classify-finding.sh +32 -6
- package/scripts/classify-finding.test.sh +23 -0
- package/scripts/classify-test-run.sh +69 -0
- package/scripts/classify-test-run.test.sh +90 -0
- package/scripts/close-parent-c3.test.sh +2 -0
- package/scripts/consumer-sweep.sh +569 -0
- package/scripts/consumer-sweep.test.sh +357 -0
- package/scripts/detect-append-registries.sh +106 -0
- package/scripts/detect-append-registries.test.sh +64 -0
- package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
- package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
- package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
- package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
- package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
- package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
- package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
- package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
- package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
- package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
- package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
- package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
- package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
- package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
- package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
- package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
- package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
- package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
- package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
- package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
- package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
- package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/lib/telemetry.sh +1 -0
- package/scripts/remediate-approved-head.test.sh +2 -0
- package/scripts/remediate-basesync.test.sh +1 -1
- package/scripts/review-acceptance-gate.sh +2 -2
- package/scripts/review-acceptance-gate.test.sh +20 -0
- package/scripts/review-delta.sh +8 -1
- package/scripts/review-delta.test.sh +19 -0
- package/scripts/review-finalize.sh +193 -41
- package/scripts/review-finalize.test.sh +211 -4
- package/scripts/review-pr-delta-wiring.test.sh +13 -0
- package/scripts/review-prepare.sh +5 -3
- package/scripts/review-prepare.test.sh +18 -2
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/telemetry.test.sh +2 -0
- package/scripts/trusted-comments.test.sh +2 -1
- package/scripts/verify-phase-trail.test.sh +8 -0
- package/scripts/wait-ci-green.sh +61 -9
- package/scripts/wait-ci-green.test.sh +41 -3
- package/scripts/with-run-tmpdir.sh +20 -0
|
@@ -179,7 +179,7 @@ EXISTING_FAST_PATH=$(printf '%s' "$CYCLE_SLICE" | bash "$TRUSTED_SCRIPT" bodies
|
|
|
179
179
|
- If `<!-- FORGE:BUILDER:COMPLETE -->` is present in a BUILDER comment of the current-cycle slice → the builder finished, but COMPLETE is posted at validate V5, BEFORE the B6.5 acceptance gate, so it is not proof the build passed. **Gate resume** (checked first; first match wins) <!-- Added: forge#3725 -->: read the newest trusted, comment of the current-cycle slice that starts with `FORGE:ACCEPTANCE_GATE` or `FORGE:BUILD_BLOCKED`, through the same anchored, trust-filtered lookup as the FAST_PATH read above (script resolution block verbatim; pipe `cycle_slice` into `bodies '^<!-- FORGE:(ACCEPTANCE_GATE|BUILD_BLOCKED)'`, and compare by comment order, never by presence):
|
|
180
180
|
- Lookup failed (`trusted-comments.sh` unresolved or `gh` error) → Blocked exit with blocker `gate-lookup-unavailable: could not read FORGE:ACCEPTANCE_GATE` (a transient GitHub failure uses the `github-unavailable:` prefix). Never read a failed lookup as "no gate marker".
|
|
181
181
|
- Newest is `FORGE:ACCEPTANCE_GATE:FAILED`, `FORGE:ACCEPTANCE_GATE:BLOCKED`, or a `FORGE:BUILD_BLOCKED` comment → the gate is blocked and the engine fails closed on it, so returning ALREADY_DONE would deadlock every re-run. Derive BRANCH/WORKTREE_PATH/PR_BASE as in B1A/B1C (without creating anything), verify the branch and worktree exist and that `git -C "$WORKTREE_PATH" branch --show-current` equals BRANCH (missing or mismatched → Blocked exit `worktree-missing: cannot re-enter B6.5`; never recreate silently), recover `COMPLEXITY_BAND` from `EXISTING_FAST_PATH` with the B0.5 resume extraction, then skip B0.5-B6 and re-enter at **Phase B6.5**, which posts a fresh PASSED/FAILED/BLOCKED verdict (the one-repair `FORGE:ACCEPTANCE_REPAIR` bound still applies).
|
|
182
|
-
- Newest is `FORGE:ACCEPTANCE_GATE:PASSED
|
|
182
|
+
- Newest is `FORGE:ACCEPTANCE_GATE:PASSED` or `FORGE:ACCEPTANCE_GATE:DEGRADED` (pass-class: the build completed, review gates the merge on the degraded paths), or no gate marker exists → build already complete. Derive BRANCH/WORKTREE_PATH as in B1A/B1C (without creating anything), then print `BUILD_RESULT: status: ALREADY_DONE`.
|
|
183
183
|
- If `<!-- FORGE:BUILDER -->` exists in the current-cycle slice BUT `<!-- FORGE:BUILDER:COMPLETE -->` is ABSENT → build was interrupted after the comment was posted but before the commit (validate V5). Delete the partial comment and restart from Phase B2 (contract): <!-- Added: forge#1305 -->
|
|
184
184
|
Select with ONE anchored, trusted selector (body must start with the marker; author must be a Bot or OWNER/MEMBER/COLLABORATOR). Delete ONLY when exactly one non-complete candidate exists and it carries no other FORGE phase marker; unparsable, zero, or multiple matches delete nothing (fail closed). <!-- Added: forge#3668 -->
|
|
185
185
|
```bash
|
|
@@ -755,8 +755,13 @@ CHANGED_FILES=$(git -C "{WORKTREE_PATH}" diff --name-only "origin/{PR_BASE}...HE
|
|
|
755
755
|
```
|
|
756
756
|
|
|
757
757
|
**After subcommand returns** (read its `VALIDATE_RESULT:` block):
|
|
758
|
-
- `gate_passed: true` →
|
|
759
|
-
- `gate_passed: false` → the
|
|
758
|
+
- `gate_passed: true` → record `gate_status` (`PASS` or `DEGRADED`) and `degraded_checks` from the `VALIDATE_RESULT:` block for B6.5 (the FORGE:QUALITY_GATE `**Degraded**:` line is the source of truth); a DEGRADED gate never blocks the build. Verify the `FORGE:QUALITY_GATE` marker exists on the issue (posted by validate V5; docs-only changes exempt). If absent, re-derive `{CHANGED_FILES}` with the B6 command above and re-invoke validate once with the full B6 `Skill(...)` call, including `--files "{CHANGED_FILES}"` (never omit `--files`; an empty list falls back to the git diff); a missing marker is not a pass. Read the job ids under `### Deferred to CI` in the trusted `FORGE:BUILDER` comment (jobs the local CI gate could not prove; remote CI at merge must) for `ci_deferred` in the result. Then continue to Phase B6.5 (acceptance gate)
|
|
759
|
+
- `gate_passed: false` with a `blocker` starting `consumer_unaddressed` → the consumer sweep failed in `blocking` mode. This is a repair, never BLOCKED and never `needs-human` <!-- Added: forge#4017 -->: remove `needs-human` if one is present, then run **one** repair round guarded by the `<!-- FORGE:CONSUMER_REPAIR: issue={NUMBER} -->` marker (count the markers first; if one already exists the round is spent, so go to step 3):
|
|
760
|
+
1. Post the marker with the `UNADDRESSED:` site list from the blocker.
|
|
761
|
+
2. Invoke `Skill(skill="{FORGE_SKILL_PREFIX}work-on:build:implement", args="{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\" --worktree {WORKTREE_PATH} --branch {BRANCH} --base {PR_BASE} --fix-consumers \"<UNADDRESSED lines>\"")`, then re-derive `{CHANGED_FILES}` with the B6 command above (never reuse the pre-repair value).
|
|
762
|
+
3. Re-invoke validate with the full B6 `Skill(...)` call plus `--consumers-final`. That run sweeps in `advisory` mode, so sites that are still unaddressed are recorded in the FORGE:BUILDER comment as `CONSUMERS: unresolved ...` and travel to review as known risk; the gate then passes or fails on its other checks only. Handle its `VALIDATE_RESULT` with the other rules in this list (a `consumer_unaddressed` blocker cannot recur, because `--consumers-final` never fails the sweep).
|
|
763
|
+
- `gate_passed: false` with a `blocker` starting `ci-local:` → the subcommand has already posted its comment and added `needs-human`; print `BUILD_RESULT: status: BLOCKED` with the child's `blocker` (run the Blocked exit if no comment was posted). This blocker (validate V4.8, the local CI gate) is final: validate already spent its 2 fix iterations in the same warm context, so do not re-invoke validate or repair it here <!-- Added: forge#3973 -->
|
|
764
|
+
- `gate_passed: false` (any other blocker) → the subcommand has already posted its comment and added `needs-human`; print `BUILD_RESULT: status: BLOCKED` with the child's `blocker` (run the Blocked exit if no comment was posted)
|
|
760
765
|
- Skill not found → Blocked exit, blocker "skill not found: work-on:build:validate"
|
|
761
766
|
- Returned running/backgrounded/empty (no `VALIDATE_RESULT:`) → do not end the turn to wait; re-read the `FORGE:QUALITY_GATE` marker (consume it if present) and the worktree state before re-invoking, then re-invoke the same child under the bounded re-invoke rule (at most 2, then `child-stalled: validate`); this cap is separate from the single marker-absent re-invoke above
|
|
762
767
|
|
|
@@ -818,6 +823,9 @@ The acceptance spec contains only a skip sentinel (\`type=skipped\`). No automat
|
|
|
818
823
|
```bash
|
|
819
824
|
# <Script resolution block, verbatim>
|
|
820
825
|
GREP_CLS=$(forge_resolve classify-grep-exit.sh) # empty/unresolved = every grep exit>=2 is a real FAIL (fail closed)
|
|
826
|
+
RG_SCRIPT=$(forge_resolve acceptance-red-green.sh) # empty/unresolved = every test= check is DEGRADED, never PASS <!-- Added: forge#3630 -->
|
|
827
|
+
RG_ROWS="" # per-AC red-green record rows for `test=` checks: | AC | test | base | head |
|
|
828
|
+
RG_DEGRADED_LIST="" # test= checks whose base run cannot execute; merged into DEGRADED_LIST after the gate-status block
|
|
821
829
|
GATE_PASS=true
|
|
822
830
|
CORRECTED=""
|
|
823
831
|
FAILED_CHECKS=""
|
|
@@ -865,6 +873,8 @@ while IFS= read -r check_line; do
|
|
|
865
873
|
# Anchored after the matcher field so `phase=` text inside description/target/matcher cannot hijack it; any other value or an absent token is `build` (fail closed).
|
|
866
874
|
PHASE=$(printf '%s\n' "$check_line" | sed -nE 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=("(([^"\\]|\\.)*)"|[^ ]*) matcher=("(([^"\\]|\\.)*)"|[^ ]*) phase=(build|review)( .*)?$/\7/p')
|
|
867
875
|
[ "$PHASE" = "review" ] || PHASE=build
|
|
876
|
+
# test= (optional, after phase=, before description=): the runnable test that proves the AC, red-green checked below (forge#3630). Absent = no change.
|
|
877
|
+
TEST_RG=$(printf '%s\n' "$check_line" | sed -nE 's/^ACCEPTANCE_CHECK: id=[^ ]* type=[^ ]* target=("(([^"\\]|\\.)*)"|[^ ]*) matcher=("(([^"\\]|\\.)*)"|[^ ]*) phase=(build|review) test="(([^"\\]|\\.)*)"( description=.*)?$/\8/p' | sed 's/\\"/"/g')
|
|
868
878
|
|
|
869
879
|
[ "$TYPE" = "skipped" ] && { MANUAL_CHECKS="${MANUAL_CHECKS}\n- [ ] **$ID** ($DESC)"; continue; }
|
|
870
880
|
|
|
@@ -876,6 +886,8 @@ while IFS= read -r check_line; do
|
|
|
876
886
|
MALFORMED=""
|
|
877
887
|
{ [ "$TYPE" = "command" ] || [ "$TYPE" = "behavior" ]; } && { case "$TARGET" in *'\\\\'*) MALFORMED="run of 4+ backslashes (over-escaped target)";; esac; [ -n "$MALFORMED" ] || bash -n -c "$TARGET" 2>/dev/null || MALFORMED="shell syntax error (unbalanced quotes)"; }
|
|
878
888
|
{ [ "$TYPE" = "command" ] || [ "$TYPE" = "behavior" ]; } && [ -z "$MALFORMED" ] && case "$(printf '%s' "$TARGET" | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" in none) MALFORMED='target "none" is not a command (a manual criterion must be type=skipped)';; esac
|
|
889
|
+
# A test= value that would break description= extraction or fails the same static parse is a check defect (never executed).
|
|
890
|
+
if [ -n "$TEST_RG" ]; then case "$TEST_RG" in *description=*) MALFORMED="test= contains description= (ambiguous field boundary)";; esac; [ -n "$MALFORMED" ] || bash -n -c "$TEST_RG" 2>/dev/null || MALFORMED="test= shell syntax error (unbalanced quotes)"; fi
|
|
879
891
|
if [ -n "$MALFORMED" ]; then MALFORMED_CHECKS="${MALFORMED_CHECKS}\n- **$ID** ($DESC): $MALFORMED"; continue; fi
|
|
880
892
|
|
|
881
893
|
RESULT="PASS"
|
|
@@ -900,12 +912,12 @@ while IFS= read -r check_line; do
|
|
|
900
912
|
# grep exit >= 2 is a usage/syntax error (e.g. a pattern starting with `-`), never a verdict on the code; exit 1 stays FAIL.
|
|
901
913
|
# Malformed only when stderr positively names a usage error (classify-grep-exit.sh); empty stderr (`2>/dev/null`, `-s`) or any operand error is a REAL failure.
|
|
902
914
|
[ "$RC" -ge 2 ] && MALFORMED=$(bash "$GREP_CLS" "$RC" "$ERR" "$TARGET" 2>/dev/null)
|
|
903
|
-
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && printf '%s' "$ERR" | grep -
|
|
915
|
+
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && { FIRST=${TARGET#"${TARGET%%[![:space:]]*}"}; FIRST=${FIRST%%[[:space:]]*}; [ -n "$FIRST" ] && [ "${FIRST#*/}" = "$FIRST" ] && ! command -v -- "$FIRST" >/dev/null 2>&1 && printf '%s\n' "$ERR" | grep -qE "(^|: )$(printf '%s' "$FIRST" | sed 's/[][\.*^$(){}?+|/]/\\&/g'):"; } && MALFORMED="command not found (rc 127): the target is not an executable command"
|
|
904
916
|
[ "$RC" -eq 0 ] || [ -n "$MALFORMED" ] || { RESULT="FAIL"; DETAIL="command exited non-zero: $TARGET"; }
|
|
905
917
|
else
|
|
906
918
|
ERRF=$(mktemp); OUTPUT=$(eval "$TARGET" 2>"$ERRF"); RC=$?; ERR=$(cat "$ERRF"); rm -f "$ERRF"; [ -z "$ERR" ] || OUTPUT="${OUTPUT}${OUTPUT:+$'\n'}${ERR}" # classify on stderr only, match on both
|
|
907
919
|
[ "$RC" -ge 2 ] && MALFORMED=$(bash "$GREP_CLS" "$RC" "$ERR" "$TARGET" 2>/dev/null)
|
|
908
|
-
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && printf '%s' "$ERR" | grep -
|
|
920
|
+
[ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && { FIRST=${TARGET#"${TARGET%%[![:space:]]*}"}; FIRST=${FIRST%%[[:space:]]*}; [ -n "$FIRST" ] && [ "${FIRST#*/}" = "$FIRST" ] && ! command -v -- "$FIRST" >/dev/null 2>&1 && printf '%s\n' "$ERR" | grep -qE "(^|: )$(printf '%s' "$FIRST" | sed 's/[][\.*^$(){}?+|/]/\\&/g'):"; } && MALFORMED="command not found (rc 127): the target is not an executable command"
|
|
909
921
|
if [ -n "$MALFORMED" ]; then :
|
|
910
922
|
elif ! printf '%s\n' "$OUTPUT" | grep -qE "$MATCHER"; then
|
|
911
923
|
if printf '%s\n' "$OUTPUT" | grep -qF -- "$MATCHER"; then CORRECTED="${CORRECTED}\n- **$ID**: output matched as a literal string"
|
|
@@ -925,6 +937,24 @@ while IFS= read -r check_line; do
|
|
|
925
937
|
case "$TARGET" in *'gh pr'*) DEFERRED_COUNT=$((DEFERRED_COUNT+1)); DEFERRED_CHECKS="${DEFERRED_CHECKS}\n- **$ID** ($DESC): DEFERRED (review) (no pull request exists yet)"; continue;; esac
|
|
926
938
|
fi
|
|
927
939
|
|
|
940
|
+
# Red-green proof <!-- Added: forge#3630 -->: only for a build-phase check with test= whose own head result is PASS.
|
|
941
|
+
# The helper runs test= on head, then (head RAN only) on a detached base worktree (`git worktree add --detach` at the merge-base) with head's changed test files overlaid;
|
|
942
|
+
# the worktree is removed on every exit path (trap + explicit `git worktree remove`). Output: `BASE=<..> HEAD=<..>`.
|
|
943
|
+
if [ "$RESULT" = "PASS" ] && [ -n "$TEST_RG" ]; then
|
|
944
|
+
RG_OUT=""; [ -n "$RG_SCRIPT" ] && RG_OUT=$(bash "$RG_SCRIPT" --repo-path "{WORKTREE_PATH}" --base-ref "origin/{PR_BASE}" --test-cmd "$TEST_RG" 2>/dev/null)
|
|
945
|
+
RG_BASE=$(printf '%s\n' "$RG_OUT" | sed -n 's/^BASE=\([A-Z_]*\) HEAD=\([A-Z_]*\)$/\1/p' | head -n 1)
|
|
946
|
+
RG_HEAD=$(printf '%s\n' "$RG_OUT" | sed -n 's/^BASE=\([A-Z_]*\) HEAD=\([A-Z_]*\)$/\2/p' | head -n 1)
|
|
947
|
+
[ -n "$RG_BASE" ] && [ -n "$RG_HEAD" ] || { RG_BASE=UNRUNNABLE; RG_HEAD=UNRUNNABLE; } # helper missing or unparsable: DEGRADED, never PASS
|
|
948
|
+
RG_ID=$(printf '%s' "$TEST_RG" | tr '\n\r|`\\' ' ' | cut -c1-80) # table-safe: no pipe, backtick, backslash or newline; truncated
|
|
949
|
+
RG_ROWS="${RG_ROWS}${RG_ROWS:+$'\n'}| $ID | $RG_ID | $RG_BASE | $RG_HEAD |"
|
|
950
|
+
case "$RG_HEAD:$RG_BASE" in
|
|
951
|
+
RAN:FAILED) : ;; # red on base, green on head: the test proves the AC
|
|
952
|
+
RAN:RAN) RESULT="FAIL"; DETAIL="test passes on base (it never exercised the defect): rewrite the test so it fails on the base commit and passes on head" ;;
|
|
953
|
+
FAILED:*) RESULT="FAIL"; DETAIL="test= command fails on head: $RG_ID" ;;
|
|
954
|
+
*) RG_DEGRADED_LIST="${RG_DEGRADED_LIST}${RG_DEGRADED_LIST:+; }$ID — base run cannot execute (base=$RG_BASE head=$RG_HEAD), so the test is unproven" ;; # a base run that cannot execute is DEGRADED, never PASS
|
|
955
|
+
esac
|
|
956
|
+
fi
|
|
957
|
+
|
|
928
958
|
VERIFIED_COUNT=$((VERIFIED_COUNT+1))
|
|
929
959
|
if [ "$RESULT" = "FAIL" ]; then
|
|
930
960
|
GATE_PASS=false
|
|
@@ -941,6 +971,26 @@ if [ "$VERIFIED_COUNT" -eq 0 ] && [ "$DEFERRED_COUNT" -eq 0 ]; then
|
|
|
941
971
|
fi
|
|
942
972
|
```
|
|
943
973
|
|
|
974
|
+
**Gate status (DEGRADED)** <!-- Added: forge#3628 -->: `GATE_STATUS` and `DEGRADED_LIST` come from the B6 `VALIDATE_RESULT` (`gate_status`, `degraded_checks`). When B6.5 is entered without one (B0 gate resume), read them from the `**Degraded**:` line of the newest trusted `FORGE:QUALITY_GATE` comment; an unreadable lookup is DEGRADED, never a pass. A DEGRADED gate means a test run exercised nothing, a known-slow test was skipped, or the blast-radius check did not complete. It does not fail the build: when every check passes, the gate closes with `<!-- FORGE:ACCEPTANCE_GATE:DEGRADED -->` instead of `:PASSED` and lists the degraded checks. Review must cover each one before merge (`scripts/review-finalize.sh`, `degraded-gate-unreviewed`).
|
|
975
|
+
|
|
976
|
+
```bash
|
|
977
|
+
# <Script resolution block, verbatim>
|
|
978
|
+
TRUSTED_SCRIPT=$(forge_resolve trusted-comments.sh)
|
|
979
|
+
if [ -z "${GATE_STATUS:-}" ]; then
|
|
980
|
+
GATE_STATUS=PASS; DEGRADED_LIST=""
|
|
981
|
+
if [ -z "$TRUSTED_SCRIPT" ] || ! QG_COMMENTS=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments 2>/dev/null) \
|
|
982
|
+
|| ! QG_BODIES=$(printf '%s' "$QG_COMMENTS" | bash "$TRUSTED_SCRIPT" bodies '^<!-- FORGE:QUALITY_GATE -->'); then
|
|
983
|
+
GATE_STATUS=DEGRADED; DEGRADED_LIST="quality-gate — degraded state unreadable"
|
|
984
|
+
else
|
|
985
|
+
DEGRADED_LIST=$(printf '%s\n' "$QG_BODIES" | tail -n 1 | jq -r 'select(type == "string")' | sed -n 's/^\*\*Degraded\*\*: //p' | head -n 1)
|
|
986
|
+
case "$DEGRADED_LIST" in ''|none) DEGRADED_LIST="" ;; *) GATE_STATUS=DEGRADED ;; esac
|
|
987
|
+
fi
|
|
988
|
+
fi
|
|
989
|
+
# test= checks whose base run cannot execute (forge#3630): merged after the lookup above so it cannot wipe them
|
|
990
|
+
[ -n "$RG_DEGRADED_LIST" ] && { GATE_STATUS=DEGRADED; DEGRADED_LIST="${DEGRADED_LIST:+$DEGRADED_LIST; }$RG_DEGRADED_LIST"; }
|
|
991
|
+
[ "$GATE_STATUS" = "DEGRADED" ] && [ -z "${DEGRADED_LIST:-}" ] && DEGRADED_LIST="unspecified (see FORGE:QUALITY_GATE)"
|
|
992
|
+
```
|
|
993
|
+
|
|
944
994
|
**Post gate result comment**:
|
|
945
995
|
|
|
946
996
|
```bash
|
|
@@ -961,16 +1011,19 @@ $( [ -n "$MANUAL_CHECKS" ] && printf '\n**Manual follow-up (type=skipped; not ve
|
|
|
961
1011
|
<!-- FORGE:ACCEPTANCE_GATE:BLOCKED -->"
|
|
962
1012
|
run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human"
|
|
963
1013
|
elif [ "$GATE_PASS" = "true" ]; then
|
|
1014
|
+
GATE_MARKER=PASSED; [ "${GATE_STATUS:-PASS}" = "DEGRADED" ] && GATE_MARKER=DEGRADED
|
|
964
1015
|
run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
|
|
965
|
-
## Acceptance Gate —
|
|
1016
|
+
## Acceptance Gate — ${GATE_MARKER}
|
|
966
1017
|
|
|
967
1018
|
All machine-checkable build-phase acceptance criteria verified against real behavior.
|
|
1019
|
+
$( [ "$GATE_MARKER" = "DEGRADED" ] && printf '\n**Degraded checks (verified nothing; the review must name each one under `### Degraded Paths Reviewed` before merge)**: %s\n' "$DEGRADED_LIST" )
|
|
1020
|
+
$( [ -n "$RG_ROWS" ] && printf '\n**Red-green proof (test= checks: must FAIL on base, PASS on head)**:\n\n| AC | test | base | head |\n|----|------|------|------|\n%s\n' "$RG_ROWS" )
|
|
968
1021
|
$( [ -n "$DEFERRED_CHECKS" ] && printf '\n**Deferred to review (phase=review; evaluated against the PR before merge)**:%b\n' "$DEFERRED_CHECKS" )
|
|
969
1022
|
$( [ -n "$MANUAL_CHECKS" ] && printf '\n**Manual follow-up (type=skipped; not verified at build time, confirm after merge/deploy)**:%b\n' "$MANUAL_CHECKS" )
|
|
970
1023
|
$( [ -n "$CORRECTED" ] && printf '\n**Checks corrected (malformed matcher, literal text present)**:%b\n' "$CORRECTED" )
|
|
971
1024
|
$( [ -n "$MALFORMED_CHECKS" ] && printf '\n**malformed-check (not counted as failures; regenerate the acceptance spec by re-running investigate)**:%b\n' "$MALFORMED_CHECKS" )
|
|
972
1025
|
|
|
973
|
-
<!-- FORGE:ACCEPTANCE_GATE
|
|
1026
|
+
<!-- FORGE:ACCEPTANCE_GATE:${GATE_MARKER} -->"
|
|
974
1027
|
else
|
|
975
1028
|
run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ACCEPTANCE_GATE -->
|
|
976
1029
|
## Acceptance Gate — FAILED
|
|
@@ -978,6 +1031,7 @@ else
|
|
|
978
1031
|
The following acceptance checks did not pass:
|
|
979
1032
|
|
|
980
1033
|
$(echo -e "$FAILED_CHECKS")
|
|
1034
|
+
$( [ -n "$RG_ROWS" ] && printf '\n**Red-green proof (test= checks: must FAIL on base, PASS on head)**:\n\n| AC | test | base | head |\n|----|------|------|------|\n%s\n' "$RG_ROWS" )
|
|
981
1035
|
$( [ -n "$MALFORMED_CHECKS" ] && printf '\n**malformed-check (not counted as failures; regenerate the acceptance spec by re-running investigate)**:%b\n' "$MALFORMED_CHECKS" )
|
|
982
1036
|
|
|
983
1037
|
Merge is blocked. Fix the failing criteria and re-run the validate phase.
|
|
@@ -989,7 +1043,7 @@ fi
|
|
|
989
1043
|
|
|
990
1044
|
A `malformed-check` (a command/behavior target with a run of 4+ backslashes or a `bash -n` syntax error, or a grep target exiting >= 2 for a reason other than a missing/unreadable operand file, which stays a real FAIL) is reported in the comment and never sets `GATE_PASS=false` by itself, never enters `FAILED_CHECKS`, is never passed to `--fix-acceptance`, and never adds `needs-human` by itself; the gate still posts exactly one of PASSED/FAILED. The gate never posts PASSED with zero verified checks: if every check was malformed (`VERIFIED_COUNT=0` and `DEFERRED_COUNT=0`) it is a spec-defect: it posts `FORGE:ACCEPTANCE_GATE:BLOCKED`, adds `needs-human`, prints `BUILD_RESULT: status: BLOCKED` (blocker: "regenerate acceptance spec via investigate") and skips the `--fix-acceptance` repair entirely, because implement cannot fix a spec defect. <!-- Added: forge#3694, forge#3825 -->
|
|
991
1045
|
|
|
992
|
-
Also `malformed-check` <!-- Added: forge#3825 -->: a `command`/`behavior` target that is literally `none` (any case), or
|
|
1046
|
+
Also `malformed-check` <!-- Added: forge#3825 -->: a `command`/`behavior` target that is literally `none` (any case), or whose own first word is not a command (exit 127 with `<first word>:` leading the stderr message and the word a bare name (no `/`) not resolvable via `command -v`; the target runs in its native locale, so verdict-bearing output is never altered and the check does not depend on the English message text). Classification keys on that stderr text, never on the code alone, and only on the target's first word: a script whose inner command is missing (`bash x.sh` exiting 127 on a tool inside it) stays a real FAIL, as does a leading env assignment or compound (`FOO=1 cmd`, `a && missing`). A missing script (a `bash` run of a nonexistent script path, with `No such file or directory`) also exits 127 and stays a real FAIL, so the missing-operand class is not reopened. A per-check `type=skipped` is never executed, never counted, and is listed under "Manual follow-up" as unchecked items in the PASSED comment.
|
|
993
1047
|
|
|
994
1048
|
A `phase=review` check (or, for legacy specs, tagged or not, a `command`/`behavior` check on a `gh pr` target whose output contains `no pull requests found`) is recorded as `DEFERRED (review)` in the PASSED comment: it is never executed here, never counted in `VERIFIED_COUNT`, never sets `GATE_PASS=false`, never enters `FAILED_CHECKS` or `--fix-acceptance`, and never spends the repair. A spec whose checks are all deferred posts PASSED (it is not "No Spec Found"). Review R2.5 evaluates the deferred checks against the real PR and blocks merge on failure. <!-- Added: forge#3793 -->
|
|
995
1049
|
|
|
@@ -1010,6 +1064,8 @@ If `GATE_PASS = false` and `SPEC_DEFECT` is not `true`, the build repairs itself
|
|
|
1010
1064
|
3. Re-run this whole B6.5 gate. PASS → continue to the checkpoint.
|
|
1011
1065
|
4. Still failing (or already repaired once) → leave `needs-human` and print `BUILD_RESULT: status: BLOCKED`, blocker: "Acceptance gate failed after one repair — see FORGE:ACCEPTANCE_GATE comment".
|
|
1012
1066
|
|
|
1067
|
+
**Red-green proof (`test=`)** <!-- Added: forge#3630 -->: a `phase=build` check carrying `test="<cmd>"` whose own head result is PASS also runs `scripts/acceptance-red-green.sh`: the test must FAIL on the merge-base commit (head's changed test files overlaid, so a new test file is not a false red) and PASS on head. Every such check gets a row in the gate comment's `| AC | test | base | head |` table. A test that passes on base FAILS the gate (`passes on base`) and enters the one-repair `--fix-acceptance` path with the instruction to rewrite the test so it fails on base. A base run that cannot execute (worktree or overlay failure, timeout, rc 126/127, a runner that exercised nothing, or an unresolved helper) is DEGRADED, never PASS: it is appended to `DEGRADED_LIST` and the gate closes `FORGE:ACCEPTANCE_GATE:DEGRADED` (#3628). A `test=` that contains `description=` or fails `bash -n` is a `malformed-check`. Checks without `test=` evaluate exactly as before.
|
|
1068
|
+
|
|
1013
1069
|
If `GATE_PASS = true`: continue to write the phase checkpoint below.
|
|
1014
1070
|
|
|
1015
1071
|
**When the gate passed — write machine-readable phase checkpoint before returning (MANDATORY)**:
|
|
@@ -1033,10 +1089,11 @@ BUILD_RESULT:
|
|
|
1033
1089
|
status: COMPLETE | ALREADY_DONE | INVESTIGATION_COMPLETE | BLOCKED
|
|
1034
1090
|
branch: {BRANCH}
|
|
1035
1091
|
worktree: {WORKTREE_PATH}
|
|
1092
|
+
ci_deferred: [{job id}, ...] # deferred-to-ci jobs from FORGE:BUILDER; empty when none
|
|
1036
1093
|
blocker: {description if status=BLOCKED}
|
|
1037
1094
|
```
|
|
1038
1095
|
|
|
1039
|
-
- `COMPLETE` — B0–B6.5 all passed; `FORGE:BUILDER:COMPLETE` is on the issue.
|
|
1096
|
+
- `COMPLETE` — B0–B6.5 all passed (a DEGRADED acceptance gate, `FORGE:ACCEPTANCE_GATE:DEGRADED`, counts as passed here); `FORGE:BUILDER:COMPLETE` is on the issue.
|
|
1040
1097
|
- `ALREADY_DONE` — B0 found `FORGE:BUILDER:COMPLETE`.
|
|
1041
1098
|
- `INVESTIGATION_COMPLETE` — the implement child created the deliverable issues (B5); no review follows.
|
|
1042
1099
|
- B5.5 never produces a `BUILD_RESULT` status of its own: its scope-check outcome (`OK`, `EXTENDED_REVIEW`, `SCOPE_FLAGGED`, `OVERRIDDEN`) is recorded in `FORGE:DIFF_SIZE`, then the build continues to B6 and ends `COMPLETE`. A finished build is never routed to decompose or discarded. <!-- Added: forge#3862 -->
|
|
@@ -715,6 +715,13 @@ ACCEPTANCE_CHECK: id=ac-7 type=command target="gh pr view --json body --jq .body
|
|
|
715
715
|
|
|
716
716
|
**Phase tag (MANDATORY)** <!-- Added: forge#3793 -->: `phase=` is an optional token placed after `matcher="..."` and before `description=`; allowed values are `build` (default when absent) and `review`. `build` checks run in the build acceptance gate, against the worktree. `review` checks are deferred by the build gate and evaluated by review after the PR exists. Any check whose target reads a PR artifact (`gh pr view`, `gh pr checks`, `gh pr diff`, review comments) MUST be `phase=review`: the PR does not exist until the review phase opens it, so such a check can never pass at build time. Do not put `phase=` inside `target`, `matcher` or `description` text.
|
|
717
717
|
|
|
718
|
+
**Test tag (optional)** <!-- Added: forge#3630 -->: `test="<cmd>"` is an optional token placed after `phase=` and before `description=`. It names the runnable test that proves the criterion (for example `test="node --test bin/tests/foo.test.mjs"`). The build gate runs it on the head worktree and on the merge-base commit: a real proof FAILS on base and PASSES on head, a test that also passes on base fails the gate (it never touched the defect), and a base run that cannot execute makes the gate DEGRADED. Quote it like `target=`, escape an embedded `"` as `\"`, and never write the text `description=` inside it. Emit it only on `phase=build` checks whose test is runnable by a single command; it is forbidden on `phase=review` and `type=skipped` checks (never executed). Without `test=` the check evaluates exactly as before.
|
|
719
|
+
|
|
720
|
+
Example:
|
|
721
|
+
```
|
|
722
|
+
ACCEPTANCE_CHECK: id=ac-3 type=command target="node --test bin/tests/foo.test.mjs" matcher="exit_0" phase=build test="node --test bin/tests/foo.test.mjs" description=Rejects an empty input
|
|
723
|
+
```
|
|
724
|
+
|
|
718
725
|
**Check types**:
|
|
719
726
|
- `exists` — assert a file or directory exists (`target` = path, `matcher` = ignored)
|
|
720
727
|
- `contains` — assert a file contains a string or regex (`target` = file path, `matcher` = string/regex). **Prefer a literal string** for code: the matcher is applied as an extended regex first, so `$`, `(`, `[`, `.`, `*`, `+`, `?`, `|` must be escaped if you mean them literally (e.g. `return \"\$HELD\"`). The acceptance gate falls back to a literal match, but a correct matcher avoids a needless repair round. For a `command` check that looks for code, use `grep -qF '<literal>' <file>`, not `grep -qE`.
|
|
@@ -31,7 +31,7 @@ background: false
|
|
|
31
31
|
|
|
32
32
|
**Scope note**: This mode owns exactly one gap — re-driving a `needs-human` or `workflow:remediating` PR's own remediation, plus resuming an interrupted M7 auto-land from `workflow:awaiting-merge` when no `FORGE:REMEDIATION:COMPLETE` trail exists (forge#3647; M1-M6 are skipped, only M7/M8 run), finishing a merged PR whose M7 run was cut off before M8 (forge#3685), and re-entering a held resume (the `workflow:remediating` state is the non-human "autonomous remediation pending" state review sets for ci-gate, in-pr-fix and base-sync handoffs, forge#3541). It does NOT implement the `needs-human` sub-label taxonomy (#1815) and does NOT edit `review-pr.md`'s Phase 8 guard (forge#1810), whose safe-default (`workflow:awaiting-merge` on a clean re-review of a previously-escalated PR) is reused as-is; this file only adds a bar-check after it fires.
|
|
33
33
|
|
|
34
|
-
**Engine coverage** (forge#2379, #2889): this subcommand's `command` name (`work-on/remediate`) and completion marker (`FORGE:REMEDIATION:COMPLETE`, including the `**Re-gate outcome**` field Phase M8 posts below) are registered in the headless engine's phase table — `RESERVED_TYPES.REMEDIATION` in `packages/protocol/src/types.js`, `remediate` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and a matching `remediate` entry in `bin/engine/phases.mjs`'s `PHASES` array. The non-terminal held-resume record `FORGE:REMEDIATION_HELD` (forge#3685) is NOT a completion marker; the engine's `detectOutcome` reads it as `terminalReason: "awaiting-merge"
|
|
34
|
+
**Engine coverage** (forge#2379, #2889): this subcommand's `command` name (`work-on/remediate`) and completion marker (`FORGE:REMEDIATION:COMPLETE`, including the `**Re-gate outcome**` field Phase M8 posts below) are registered in the headless engine's phase table — `RESERVED_TYPES.REMEDIATION` in `packages/protocol/src/types.js`, `remediate` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and a matching `remediate` entry in `bin/engine/phases.mjs`'s `PHASES` array. The non-terminal held-resume record `FORGE:REMEDIATION_HELD` (forge#3685) is NOT a completion marker; the engine's `detectOutcome` reads it as `terminalReason: "awaiting-merge"`, except a `Hold reason` of `CI pending`, which it reads as a wait (`wait: "ci-pending"`): the engine polls CI with backoff and re-dispatches this resume (forge#4018). A blocked review is committed with `terminalReason: "needs-human"`, then the engine continues directly into remediation; the divergence guard permits this specific handoff while keeping all other `needs-human` states paused. Any `**Re-gate outcome**` outside the Output list fails the attempt; `INPR-FIX-REQUESTED` (with `**INPR round**: N`) re-arms one further in-PR fix remediation instead of parking (forge#4021).
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
@@ -132,11 +132,14 @@ if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$PR_COMMENTS" ] && [ -n "$ISSUE_COMMENTS" ]
|
|
|
132
132
|
[.[][] | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | select((.body // "") | type == "string")] as $c
|
|
133
133
|
| [$c[] | select(.body | test("\\A<!-- FORGE:(CI|INPR|BASESYNC)_REMEDIATION: pr=" + $pr + " -->")) | {t: .created_at, k: (.body | capture("FORGE:(?<k>CI|INPR|BASESYNC)_REMEDIATION").k + "_REMEDIATION")}] as $m
|
|
134
134
|
| [$c[] | select(.body | test("\\A<!-- FORGE:REMEDIATION -->[\\s\\S]*<!-- FORGE:REMEDIATION:COMPLETE -->")) | .created_at] as $trails
|
|
135
|
-
|
|
|
135
|
+
| ([$c[] | select(.body | test("\\A<!-- FORGE:INPR_FIX: round=[0-9]+ head=")) | .created_at] | max // "") as $fix
|
|
136
|
+
| [$trails[] as $t | select($kind != "INPR_REMEDIATION" or $fix == "" or $t > $fix) | ([$m[] | select(.t <= $t)] | sort_by(.t) | last | .k // "") | select(. == $kind)] | length' 2>/dev/null || echo "")
|
|
136
137
|
if [ -n "$KIND_DONE_N" ] && [ "$KIND_DONE_N" -eq 0 ]; then REMEDIATION_COMPLETE_N=0; fi
|
|
137
138
|
fi
|
|
138
139
|
```
|
|
139
140
|
|
|
141
|
+
**Re-armed INPR round** <!-- Added: forge#4037 -->: for `INPR_REMEDIATION` a completed trail counts only when it is newer than the newest trusted `FORGE:INPR_FIX: round=N head=` work order (the `$fix` bound above), so the re-gate's second in-PR round (a newer `INPR_FIX` than the newest trail) dispatches instead of returning `ALREADY_DONE`; with no work order the per-kind rule above is unchanged. The router bounds re-arms to one.
|
|
142
|
+
|
|
140
143
|
Completed trails of other kinds are left in place (never deleted); only the interrupted-partial cleanup below deletes anything.
|
|
141
144
|
|
|
142
145
|
- If `REMEDIATION_COMPLETE_N` or `REMEDIATION_TRAIL_N` is empty (comments or `scripts/trusted-comments.sh` unreadable or unresolvable) → fail closed: EXIT `REMEDIATE_RESULT: status: BLOCKED`, blocker: "github-unavailable: remediation trail unreadable (comments or scripts/trusted-comments.sh unresolvable)". Never treat unreadable as "no trail" and never as `ALREADY_DONE`.
|
|
@@ -198,7 +201,7 @@ BLOCK_COMMENTS=$(gh api repos/{GH_REPO}/issues/{ISSUE_NUMBER}/comments \
|
|
|
198
201
|
```
|
|
199
202
|
|
|
200
203
|
**Classify into FIXABLE vs. UNFIXABLE**:
|
|
201
|
-
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a base conflict (`base-conflict`: `CONFLICTING`/`DIRTY`, or `BEHIND` under required-up-to-date protection — resolvable by merging `origin/{PR_BASE}` in, the base-sync step in Phase M3; `BLOCKED` alone is branch protection that a sync cannot clear and is UNFIXABLE unless a conflict state is also present), a quality-gate/build failure, or a CI-gate refusal (`ci gate not green` — the failing/cancelled/timed-out checks are listed on the linked issue; read each failing job's log with `gh run view --log-failed`, fix the cause on the PR branch, or re-run a check that failed for an infrastructure reason), or an in-PR fix request (`in-pr fix required` — review-pr §6B.6 posted a `<!-- FORGE:INPR_FIX: round=1 head=<sha> max_severity=<MEDIUM|HIGH|CRITICAL> findings=<ids> -->` comment on the PR listing CONFIRMED MEDIUM/HIGH/CRITICAL findings in files this PR changed. That comment is the work order: fix exactly the listed findings, at the listed `file:line`, and nothing else. Do not file them as issues; the re-review in Phase M6 files any that remain).
|
|
204
|
+
- **FIXABLE** — open `review-finding` issues (CONFIRMED/LIKELY code defects), a `VERDICT=CHANGES REQUESTED` block with concrete findings attached, a base conflict (`base-conflict`: `CONFLICTING`/`DIRTY`, or `BEHIND` under required-up-to-date protection — resolvable by merging `origin/{PR_BASE}` in, the base-sync step in Phase M3; `BLOCKED` alone is branch protection that a sync cannot clear and is UNFIXABLE unless a conflict state is also present), a quality-gate/build failure, or a `degraded-gate-unreviewed` refusal (not a code fix: re-run the review so its `### Degraded Paths Reviewed` section names each degraded check), or a CI-gate refusal (`ci gate not green` — the failing/cancelled/timed-out checks are listed on the linked issue; read each failing job's log with `gh run view --log-failed`, fix the cause on the PR branch, or re-run a check that failed for an infrastructure reason), or an in-PR fix request (`in-pr fix required` — review-pr §6B.6 posted a `<!-- FORGE:INPR_FIX: round=1 head=<sha> max_severity=<MEDIUM|HIGH|CRITICAL> findings=<ids> -->` comment on the PR listing CONFIRMED MEDIUM/HIGH/CRITICAL findings in files this PR changed. That comment is the work order: fix exactly the listed findings, at the listed `file:line`, and nothing else. Do not file them as issues; the re-review in Phase M6 files any that remain).
|
|
202
205
|
- **UNFIXABLE (policy escalation)** — `HAS_PURPOSE_REGRESSION=true` (the PR's behavior diverges from the issue's intent — a judgment call, not a code defect), `CALIBRATION_NEEDS_HUMAN=true` (statistical trust threshold), or `TRUST_NEEDS_HUMAN=true` (provenance `NOVEL_NEEDS_HUMAN` tier, insufficient prior data — a policy gate, not a bug). None of these are mechanically "fixable" by re-editing code.
|
|
203
206
|
|
|
204
207
|
**If the block reason classifies as UNFIXABLE** (and no FIXABLE item accompanies it): do NOT attempt any fix. Skip directly to Phase M8 with verdict `UNFIXABLE`, re-affirm `needs-human` (add it and remove `workflow:remediating` if the issue entered as `workflow:remediating`), and return `REMEDIATE_RESULT: status: UNFIXABLE`. This satisfies AC5 — "genuinely-blocked PRs still terminate at `needs-human`."
|
|
@@ -500,8 +503,12 @@ else:
|
|
|
500
503
|
|
|
501
504
|
Wait for the completed child result and retain its `REVIEW_RESULT` in remediation state before continuing to Phase M7. A running/progress response is not a completion result. If the child errors or does not return a parseable `REVIEW_RESULT`, stop with `REMEDIATE_RESULT: status: BLOCKED`; do not report remediation in progress as a terminal parent result. The `Skill(...)` invocation above remains the non-OpenCode path.
|
|
502
505
|
|
|
506
|
+
If the re-review approved but requested a further in-PR fix round (forge#4021): the retained `REVIEW_RESULT` is not `BLOCKED` and a trusted (Bot or OWNER/MEMBER/COLLABORATOR) `<!-- FORGE:INPR_FIX: round=N head=<sha> ... -->` comment, posted on the PR after this Phase M6 started, names the live head. Not a hold or escalation: set `RE_GATE_OUTCOME="INPR-FIX-REQUESTED"` and `INPR_ROUND=N`, do NOT add `needs-human`, skip Phase M7, go to Phase M8 and return `REMEDIATE_RESULT: status: COMPLETE`. The caller dispatches one further in-PR fix remediation; a second request is bounded by the `INPR_REMEDIATION` marker and escalates. <!-- Added: forge#4021 -->
|
|
507
|
+
|
|
503
508
|
If the retained `REVIEW_RESULT` is `PHASE_TRAIL_FAILED` (forge#3102): the gate refused to merge on missing phase markers, not on a code finding. This phase does NOT re-dispatch the missing phases: it is a dispatching phase running in a fork (`docs/WORK-ON-RUNTIME.md` R1), and `work-on:review` Phase R4 is the single re-dispatch owner. Keep `needs-human`, post `<!-- FORGE:PHASE_TRAIL_FAILED -->` listing the missing markers from the `MISSING:` lines, and exit `REMEDIATE_RESULT: status: BLOCKED` with blocker "phase trail incomplete" so the router can hand the `MISSING:` lines back to the review owner. Never treat it as `HELD-AWAITING-MERGE` or `AUTO-LANDED`. A still-failing trail can also be cleared by a human `<!-- FORGE:PHASE_TRAIL_OVERRIDE -->` comment (forge#3152, see `scripts/verify-phase-trail.sh -h`); the verifier decides, this phase never posts one.
|
|
504
509
|
|
|
510
|
+
If the retained `REVIEW_RESULT` is `BLOCKED` with blocker `degraded-gate-unreviewed` (forge#3628): the build gate was DEGRADED and the review's `### Degraded Paths Reviewed` section does not name every degraded check. It is a review gap, not a code defect, so do not edit code for it. Keep `needs-human` (Phase 8 set it), do not merge, and exit `REMEDIATE_RESULT: status: BLOCKED` with the same blocker; `work-on:review` R4 owns the single re-review that adds the section.
|
|
511
|
+
|
|
505
512
|
If the retained `REVIEW_RESULT` is `BLOCKED` with a blocker mentioning the phase trail (e.g. "phase trail unreadable (rc=N)": the Phase 8 verifier exited ≥2 / 127, forge#3147): the gate could not run, so nothing is missing and nothing is re-run. Keep `needs-human` (review-pr Phase 8 already re-asserted it), do not merge, and exit `REMEDIATE_RESULT: status: BLOCKED` with the same blocker.
|
|
506
513
|
|
|
507
514
|
If the retained `REVIEW_RESULT` is `BLOCKED` with blocker "base-conflict" (forge#3496): either the base moved again during the re-review, or the sync did not clear the conflict. Tell the two apart by base head, not by attempt count:
|
|
@@ -661,6 +668,8 @@ The strict `APPROVED_COUNT >= 2` verified-human requirement does NOT apply here:
|
|
|
661
668
|
|
|
662
669
|
A `RESUME_AT_M7=true` run on a deploy-gate base NEVER meets this bar (`BAR_MET=false` is enforced in the `if` below): no M3 gate ran in the resume invocation, so it holds for a human. <!-- Added: forge#3684 -->
|
|
663
670
|
|
|
671
|
+
**CI pending is a hold (forge#4018)**: `wait-ci-green.sh` rc 3 (pending) or rc 4 (rate-limited) at the CI gate below sets `RE_GATE_OUTCOME=HELD-AWAITING-MERGE` with `HOLD_REASON="CI pending..."` and posts no refusal and no `needs-human`; only rc 1/2 and the head-binding refusal re-escalate.
|
|
672
|
+
|
|
664
673
|
**Evaluate the base-scoped bar**:
|
|
665
674
|
```bash
|
|
666
675
|
# Fresh shell: re-derive the live base (as above); :-true fail-closed default.
|
|
@@ -689,6 +698,8 @@ if [ -n "$CI_GATE_SCRIPT" ]; then CI_GATE_OUT=$(bash "$CI_GATE_SCRIPT" {PR_NUMBE
|
|
|
689
698
|
else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
|
|
690
699
|
echo "$CI_GATE_OUT"
|
|
691
700
|
GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
|
|
701
|
+
# rc 3 (TIMEOUT: checks still pending) and rc 4 (RATE_LIMITED: GitHub throttled or briefly down) are waits, not failures: captured BEFORE the local head check below reuses rc 4.
|
|
702
|
+
CI_WAIT=false; case "$CI_GATE_RC" in 3|4) CI_WAIT=true ;; esac
|
|
692
703
|
# forge#3726 / forge#3686: on EVERY path the merge must land the exact SHA the approval was bound to. Reload the persisted head (never re-read the PR here); unset/invalid fails closed.
|
|
693
704
|
_SD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"
|
|
694
705
|
APPROVED_HEAD=$(head -n 1 "$_SD/remediate_approved_head_{PR_NUMBER}" 2>/dev/null || true)
|
|
@@ -698,8 +709,10 @@ if [ "$CI_GATE_RC" -eq 0 ] && { [ -z "${APPROVED_HEAD:-}" ] || [ "$GATED_HEAD" !
|
|
|
698
709
|
CI_GATE_OUT="${CI_GATE_OUT}
|
|
699
710
|
CI_GATE: head ${GATED_HEAD:-unknown} is not the approved head ${APPROVED_HEAD:-none} (fail closed)"; CI_GATE_RC=4
|
|
700
711
|
fi
|
|
701
|
-
|
|
702
|
-
|
|
712
|
+
if [ "$CI_WAIT" = "true" ]; then
|
|
713
|
+
# CI pending / rate-limited: hold, never needs-human, no refusal comment. The engine polls CI (no model) and re-dispatches this resume.
|
|
714
|
+
RE_GATE_OUTCOME="HELD-AWAITING-MERGE"; HOLD_REASON="CI pending (gate rc=${CI_GATE_RC}); nothing to fix, the re-gate resumes once CI settles"
|
|
715
|
+
elif [ "$CI_GATE_RC" -ne 0 ]; then
|
|
703
716
|
# Not green: do not land. Re-escalate with the gate output (counts as RE-ESCALATED below).
|
|
704
717
|
CI_MSG="⛔ Remediation auto-land refused for PR #{PR_NUMBER}: CI is not green (rc=${CI_GATE_RC}).
|
|
705
718
|
\`\`\`
|
|
@@ -734,7 +747,7 @@ fi
|
|
|
734
747
|
|
|
735
748
|
## Phase M8: Finalize FORGE:REMEDIATION Paper Trail
|
|
736
749
|
|
|
737
|
-
Post the completion body to **both** `{PR_NUMBER}` and `{ISSUE_NUMBER}` — this is the single idempotency marker checked by Phase M0 (this file, on future resume) and by the orchestrator's item 6.4 dispatch guard. **Exception (forge#3685)**: a `RESUME_AT_M7=true` run whose outcome is `HELD-AWAITING-MERGE` posts a `<!-- FORGE:REMEDIATION_HELD -->` record (carrying `**Re-gate outcome**` and the hold reason) and NO `FORGE:REMEDIATION:COMPLETE`, so a transient hold (CI pending, stale approval) does not strand the PR behind the single-attempt guard. AUTO-LANDED, RE-ESCALATED, UNFIXABLE, REREVIEW-REQUIRED and non-resume HELD keep COMPLETE:
|
|
750
|
+
Post the completion body to **both** `{PR_NUMBER}` and `{ISSUE_NUMBER}` — this is the single idempotency marker checked by Phase M0 (this file, on future resume) and by the orchestrator's item 6.4 dispatch guard. **Exception (forge#3685)**: a `RESUME_AT_M7=true` run, or a first run held on `CI pending`, whose outcome is `HELD-AWAITING-MERGE` posts a `<!-- FORGE:REMEDIATION_HELD -->` record (carrying `**Re-gate outcome**` and the hold reason) and NO `FORGE:REMEDIATION:COMPLETE`, so a transient hold (CI pending, stale approval) does not strand the PR behind the single-attempt guard. AUTO-LANDED, RE-ESCALATED, UNFIXABLE, REREVIEW-REQUIRED, INPR-FIX-REQUESTED and non-resume HELD (other than CI pending) keep COMPLETE:
|
|
738
751
|
|
|
739
752
|
```bash
|
|
740
753
|
# Fresh shell: re-derive the live base (as above); :-true fail-closed default.
|
|
@@ -757,9 +770,14 @@ case "$RE_GATE_OUTCOME" in
|
|
|
757
770
|
RE-ESCALATED) AUTO_LAND_BAR_TEXT="N/A — re-escalated before the bar was evaluated"; OUTCOME_DETAIL="at needs-human" ;;
|
|
758
771
|
UNFIXABLE) AUTO_LAND_BAR_TEXT="N/A — unfixable (see Phase M1 classification)"; OUTCOME_DETAIL="at needs-human" ;;
|
|
759
772
|
REREVIEW-REQUIRED) AUTO_LAND_BAR_TEXT="N/A — re-review not run (no sub-agent dispatch tool in this session)"; OUTCOME_DETAIL="at workflow:in-review, re-review required from a session with dispatch" ;;
|
|
760
|
-
|
|
773
|
+
INPR-FIX-REQUESTED) AUTO_LAND_BAR_TEXT="N/A — in-pr fix requested by the re-review (forge#4021)"; OUTCOME_DETAIL="at workflow:in-review, in-pr fix round ${INPR_ROUND:-1}" ;;
|
|
774
|
+
*) echo "remediate: unknown RE_GATE_OUTCOME '${RE_GATE_OUTCOME}' (a known state must have its own arm above)" >&2; AUTO_LAND_BAR_TEXT="N/A"; OUTCOME_DETAIL="" ;;
|
|
761
775
|
esac
|
|
762
776
|
|
|
777
|
+
# forge#4021: machine-readable in-PR round for the engine (empty for every other outcome).
|
|
778
|
+
INPR_ROUND_LINE=""; [ "$RE_GATE_OUTCOME" = "INPR-FIX-REQUESTED" ] && INPR_ROUND_LINE="**INPR round**: ${INPR_ROUND:-1}
|
|
779
|
+
"
|
|
780
|
+
|
|
763
781
|
# forge#3413: embed the pushed head so the orchestrator scopes a REREVIEW-REQUIRED trail to the CURRENT head only.
|
|
764
782
|
REMEDIATED_HEAD_SHA=$(gh pr view {PR_NUMBER} {GH_FLAG} --json headRefOid --jq '.headRefOid' 2>/dev/null || true)
|
|
765
783
|
|
|
@@ -794,8 +812,9 @@ if [ "$BASESYNC_KIND" = "BASESYNC_REMEDIATION" ]; then
|
|
|
794
812
|
fi
|
|
795
813
|
fi
|
|
796
814
|
|
|
797
|
-
|
|
798
|
-
|
|
815
|
+
_CI_HOLD=false; case "${HOLD_REASON:-}" in "CI pending"*) _CI_HOLD=true ;; esac
|
|
816
|
+
if { [ "${RESUME_AT_M7:-false}" = "true" ] || [ "$_CI_HOLD" = "true" ]; } && [ "$RE_GATE_OUTCOME" = "HELD-AWAITING-MERGE" ]; then
|
|
817
|
+
# forge#3685: a resume (or a first-run CI-pending hold) that ends HELD is NOT a completed attempt. Post a distinct, non-terminal record: header is
|
|
799
818
|
# FORGE:REMEDIATION_HELD (never "<!-- FORGE:REMEDIATION -->", so the M0 partial-trail cleanup and the COMPLETE/TRAIL
|
|
800
819
|
# matchers never see it) and NO FORGE:REMEDIATION:COMPLETE, so the next invocation re-enters M7 instead of ALREADY_DONE.
|
|
801
820
|
REMEDIATION_BODY="<!-- FORGE:REMEDIATION_HELD -->
|
|
@@ -803,7 +822,7 @@ if [ "${RESUME_AT_M7:-false}" = "true" ] && [ "$RE_GATE_OUTCOME" = "HELD-AWAITIN
|
|
|
803
822
|
|
|
804
823
|
**Re-gate outcome**: ${RE_GATE_OUTCOME} ${OUTCOME_DETAIL}
|
|
805
824
|
**Auto-land bar**: ${AUTO_LAND_BAR_TEXT}
|
|
806
|
-
**Hold reason**: ${HOLD_REASON:-
|
|
825
|
+
**Hold reason**: ${HOLD_REASON:-approval not bound to the live head, deploy-gate base, or a CI wait (set HOLD_REASON in M7; see its output above)}
|
|
807
826
|
**Head**: ${REMEDIATED_HEAD_SHA:-unknown}"
|
|
808
827
|
else
|
|
809
828
|
REMEDIATION_BODY="<!-- FORGE:REMEDIATION -->
|
|
@@ -813,7 +832,7 @@ REMEDIATION_BODY="<!-- FORGE:REMEDIATION -->
|
|
|
813
832
|
**Re-review verdict**: ${RE_REVIEW_VERDICT:-unknown}
|
|
814
833
|
**Auto-land bar**: ${AUTO_LAND_BAR_TEXT}
|
|
815
834
|
**Re-gate outcome**: ${RE_GATE_OUTCOME} ${OUTCOME_DETAIL}
|
|
816
|
-
**Head**: ${REMEDIATED_HEAD_SHA:-unknown}
|
|
835
|
+
${INPR_ROUND_LINE}**Head**: ${REMEDIATED_HEAD_SHA:-unknown}
|
|
817
836
|
${BASESYNC_LINE}
|
|
818
837
|
<!-- FORGE:REMEDIATION:COMPLETE -->"
|
|
819
838
|
fi
|
|
@@ -847,9 +866,9 @@ REMEDIATE_RESULT:
|
|
|
847
866
|
status: COMPLETE | ALREADY_DONE | UNFIXABLE | BLOCKED | REREVIEW_REQUIRED
|
|
848
867
|
pr_number: {PR_NUMBER}
|
|
849
868
|
issue_number: {ISSUE_NUMBER}
|
|
850
|
-
re_gate_outcome: AUTO-LANDED | HELD-AWAITING-MERGE | RE-ESCALATED | UNFIXABLE | REREVIEW-REQUIRED | N/A
|
|
869
|
+
re_gate_outcome: AUTO-LANDED | HELD-AWAITING-MERGE | RE-ESCALATED | UNFIXABLE | REREVIEW-REQUIRED | INPR-FIX-REQUESTED | N/A
|
|
851
870
|
findings_addressed: [{finding_number}, ...]
|
|
852
871
|
blocker: {description if status=BLOCKED; `deferred-live-run: ...` when M0 deferred to a live run}
|
|
853
872
|
```
|
|
854
873
|
|
|
855
|
-
**Caller behavior**: this Skill already drives its own close phase when `re_gate_outcome: AUTO-LANDED` (see Phase M8) — the caller does not need to invoke close itself (under the headless engine, the engine verifies the issue is CLOSED + `workflow:merged` and runs close if it is missing). For `re_gate_outcome: REREVIEW-REQUIRED` (`status: REREVIEW_REQUIRED`, forge#3240) the fix is pushed but no review ran because this session has no sub-agent dispatch tool: the caller MUST run `review-pr {PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE}` from a session that has dispatch (never inline) and then drive close itself on a merge. For every other `re_gate_outcome`, this result is terminal for the current invocation (a held resume, which wrote `FORGE:REMEDIATION_HELD` and not COMPLETE, can be re-invoked by an operator later and re-enters Phase M7): the issue is left at `needs-human` or `workflow:awaiting-merge`, both already recognized as terminal states in the Universal Phase Dispatcher (see `work-on.md`).
|
|
874
|
+
**Caller behavior**: this Skill already drives its own close phase when `re_gate_outcome: AUTO-LANDED` (see Phase M8) — the caller does not need to invoke close itself (under the headless engine, the engine verifies the issue is CLOSED + `workflow:merged` and runs close if it is missing). For `re_gate_outcome: REREVIEW-REQUIRED` (`status: REREVIEW_REQUIRED`, forge#3240) the fix is pushed but no review ran because this session has no sub-agent dispatch tool: the caller MUST run `review-pr {PR_NUMBER} --auto-merge --issue {ISSUE_NUMBER} --base {PR_BASE}` from a session that has dispatch (never inline) and then drive close itself on a merge. For `re_gate_outcome: INPR-FIX-REQUESTED` (`status: COMPLETE`, forge#4021) the caller dispatches one further in-PR fix remediation (engine kind `inpr-fix`, bounded by `INPR_REMEDIATION`) and never parks the first request at `needs-human`. For every other `re_gate_outcome`, this result is terminal for the current invocation (a held resume, which wrote `FORGE:REMEDIATION_HELD` and not COMPLETE, can be re-invoked by an operator later and re-enters Phase M7): the issue is left at `needs-human` or `workflow:awaiting-merge`, both already recognized as terminal states in the Universal Phase Dispatcher (see `work-on.md`).
|
|
@@ -188,6 +188,25 @@ If `LAST_VERDICT_SHA` is empty (no prior CHANGES REQUESTED verdict found) or dif
|
|
|
188
188
|
|
|
189
189
|
---
|
|
190
190
|
|
|
191
|
+
## Pre-Handoff Base Sync (before the R1 ancestry guard and the first push) <!-- Added: forge#3969 -->
|
|
192
|
+
|
|
193
|
+
Never hand off a PR born stale. If `origin/{PR_BASE}` exists and is not an ancestor of `{BRANCH}`, merge it now, while the build context is warm. Merge only: no rebase, no force.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
cd {WORKTREE_PATH}
|
|
197
|
+
PRE_SYNC_TREE=$(git rev-parse 'HEAD^{tree}') # the tree the build-time QUALITY_GATE PASS is bound to
|
|
198
|
+
if git fetch origin {PR_BASE} >/dev/null 2>&1 && ! git merge-base --is-ancestor origin/{PR_BASE} HEAD 2>/dev/null; then
|
|
199
|
+
git merge --no-edit origin/{PR_BASE} >/dev/null 2>&1 || { echo "BASE SYNC CONFLICT: resolve in this context, then git commit; on doubt git merge --abort and return BLOCKED + needs-human"; git diff --name-only --diff-filter=U; }
|
|
200
|
+
fi
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
On conflict, resolve every file, keep both sides' intent, `git add`, `git commit --no-edit`. If not resolvable with confidence: `git merge --abort`, label `needs-human`, return `REVIEW_RESULT: status: BLOCKED` with blocker "pre-handoff base sync conflict". One bounded round; resolve SHAs only after it.
|
|
204
|
+
|
|
205
|
+
**Re-gate the merged head** <!-- Added: forge#3983 -->: the `FORGE:QUALITY_GATE` PASS is bound to `HEAD^{tree}` (R1.5 `--head-tree`), so a merge that changes the tree invalidates it. After the merge and any conflict resolution, compare `PRE_SYNC_TREE` with the current `HEAD^{tree}` (fail closed if either cannot be resolved):
|
|
206
|
+
|
|
207
|
+
- Unchanged: do nothing; never re-gate.
|
|
208
|
+
- Changed: run the scoped quality gate on the merged head here (`work-on/build/validate` V1 loop over the PR's changed files), then post the marker via validate.md V5 Step 1 "Marker-only re-gate mode" (`**Tree**` = merged `HEAD^{tree}`, `**Commands run**` = what ran). Post the real result: on FAIL, fix and re-run within the validate bounds, else add `needs-human` and return `REVIEW_RESULT: status: BLOCKED`, blocker "post-sync quality gate failed"; never post PASS without a run.
|
|
209
|
+
|
|
191
210
|
## Phase R1: Pre-Push Ancestry Guard
|
|
192
211
|
|
|
193
212
|
Before pushing, verify the branch contains no merge commits that bring in history from outside the PR base. Merging `origin/{PR_BASE}` itself into the branch (a base sync) is allowed: every non-first parent of each merge in `origin/{PR_BASE}..{BRANCH}` must be an ancestor of `origin/{PR_BASE}`. This is the final defense against milestone-code-onto-staging contamination.
|
|
@@ -308,7 +327,7 @@ Print the block above (`status: BLOCKED`, blocker: "git push failed") and STOP.
|
|
|
308
327
|
|
|
309
328
|
## Phase R1.5: Phase-Trail Preflight (MANDATORY — before PR creation) <!-- Added: forge#3061 -->
|
|
310
329
|
|
|
311
|
-
A PR must not be opened for work whose earlier phases were skipped. Run the deterministic verifier; it requires INVESTIGATOR, FAST_PATH and CONTRACT always, CONTEXT and ARCHITECT unless the band is TRIVIAL/INVESTIGATION, and a passing FORGE:QUALITY_GATE unless the diff is docs-only.
|
|
330
|
+
A PR must not be opened for work whose earlier phases were skipped. Run the deterministic verifier; it requires INVESTIGATOR, FAST_PATH and CONTRACT always, CONTEXT and ARCHITECT unless the band is TRIVIAL/INVESTIGATION, and a passing FORGE:QUALITY_GATE unless the diff is docs-only. The PASS must be bound to the current `HEAD^{tree}`; the Pre-Handoff Base Sync re-gates a merged head, so re-dispatch here is only a backstop. <!-- Added: forge#3983 -->
|
|
312
331
|
|
|
313
332
|
```bash
|
|
314
333
|
# --no-renames: list BOTH sides of a rename so a file moved into docs/ cannot hide its source path (forge#3145).
|
|
@@ -547,7 +566,7 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
547
566
|
|
|
548
567
|
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker mentions the phase trail (any blocker containing "phase trail": "phase trail unreadable" when the Phase 8 verifier exited ≥2 / 127, "phase trail incomplete…", or "auto-merge requires --issue", forge#3147): the merge gate refused or could not run. Do NOT re-run phases (nothing is missing) and do NOT attempt the manual merge below, because that would bypass the gate. Add `needs-human` and return `REVIEW_RESULT: status: BLOCKED` with the same blocker. The PR stays open and unmerged.
|
|
549
568
|
|
|
550
|
-
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "stale review" (a commit landed on the PR after the verdict, or the head moved during the CI wait, so the code that would merge is not the reviewed code): do NOT merge. The re-review bound is persisted, not remembered: count `<!-- FORGE:STALE_REREVIEW: pr={PR_NUMBER} -->` comments on the issue. If the count is 0, post that marker (with the new head SHA),
|
|
569
|
+
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "stale review" (a commit landed on the PR after the verdict, or the head moved during the CI wait, so the code that would merge is not the reviewed code): do NOT merge. The re-review bound is persisted, not remembered: count `<!-- FORGE:STALE_REREVIEW: pr={PR_NUMBER} -->` comments on the issue. If the count is 0, post that marker (with the new head SHA), re-gate the new head per the Pre-Handoff Base Sync "Re-gate the merged head" procedure (the quality-gate skill alone posts no marker), then re-invoke Phase R3 once — a review of the new head (scoped by /review-pr: full, delta or carried forward). If the count is already ≥ 1 (the head keeps moving after review), add `needs-human` and return `REVIEW_RESULT: status: BLOCKED`, blocker: "PR head keeps moving after review". <!-- forge#3188 -->
|
|
551
570
|
|
|
552
571
|
- **Remediation-pending handoff (shared by the ci-gate, in-pr-fix and base-conflict cases below)** <!-- Added: forge#3541 -->: these three cases are autonomous pipeline work, not a human decision, so the issue must NOT carry `needs-human` while remediation is pending. `needs-human` is a human-gate signal (operators alert on it, and orchestrate holds DAG dependents as GATED on it). Mark the handoff with the non-human state `workflow:remediating` instead, and clear any `needs-human` that review-pr Phase 8 already set for the ci-gate refusal. `work-on:remediate` accepts either label (a bare `needs-human` from an older run still works), clears `workflow:remediating` on every exit, and re-asserts `needs-human` only when it cannot fix the PR. Run this block, then return `REVIEW_RESULT: status: NEXT`:
|
|
553
572
|
```bash
|
|
@@ -560,7 +579,9 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
560
579
|
gh pr edit {PR_NUMBER} {GH_FLAG} --remove-label "needs-human" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
561
580
|
```
|
|
562
581
|
|
|
563
|
-
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "
|
|
582
|
+
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "degraded-gate-unreviewed" (the build gate was DEGRADED, a check ran nothing, and no trusted `### Degraded Paths Reviewed` PR section names every degraded check; forge#3628): do NOT merge by any other route, and do NOT leave `needs-human` because of this gate on the first pass. Remove the `needs-human` that Phase 8 set, then re-review once on its own bound (forge#3996; never the `STALE_REREVIEW` bound, which other causes consume): count `<!-- FORGE:DEGRADED_REREVIEW: pr={PR_NUMBER} -->` (the `DEGRADED_REREVIEW` bound below), post the marker, and re-invoke Phase R3 once so `/review-pr` reads the degraded list from the issue's FORGE:QUALITY_GATE and adds the section. If the bound is already used, add `needs-human` and return `REVIEW_RESULT: status: BLOCKED`, blocker "degraded-gate-unreviewed after re-review".
|
|
583
|
+
|
|
584
|
+
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "checks still pending" (Phase 8 merge-gate timed out while CI was still running; nothing failed, #3919): do NOT remediate and do NOT add `needs-human`. Return that BLOCKED result unchanged so the caller retries the review phase, whose merge gate re-runs once CI finishes. The engine (`forgedock run-issue`) is that caller: it recognises the blocker, polls CI with `scripts/wait-ci-green.sh` and backoff (no model calls, bounded by `engine.ci_pending_max_minutes`, default 90; a rate-limited gate, `wait-ci-green.sh` rc 4, is waited out the same way, forge#4018), then re-dispatches this review phase once; budget exhaustion ends `needs-human` with `review.ci_pending_timeout`. The router and interactive caller path is unchanged.
|
|
564
585
|
|
|
565
586
|
- `REVIEW_RESULT: status: BLOCKED` from /review-pr whose blocker contains "ci gate" (Phase 8 refused to merge because checks failed or were cancelled): do NOT attempt the manual merge below — that would bypass the CI gate. Fixing red CI is pipeline work, not a human decision: hand remediation to the router **once**. Bound: count `<!-- FORGE:CI_REMEDIATION: pr={PR_NUMBER} -->` comments on the issue first; if ≥ 1, do not remediate again — remove `workflow:remediating`, label the issue `needs-human` (the bound is exhausted, so a human is now genuinely needed) and return `REVIEW_RESULT: status: BLOCKED` with blocker "ci gate not green after remediation". Otherwise run the remediation-pending handoff block above (sets `workflow:remediating`, removes the `needs-human` that Phase 8 set) and return `REVIEW_RESULT: status: NEXT` with `next: remediate` and `remediation: ci-gate` (see Output). Do not post the marker here: the router posts it immediately before it invokes remediation (Phase 4R), so a handoff interrupted before remediation starts is retried instead of being counted as used. **Never invoke `work-on:remediate` from this phase** (forge#3398): remediation re-reviews through `/review-pr`, which spawns domain-reviewer sub-agents, so it must run one level below the router, not nested inside this fork. Nested here, it lands at the sub-agent depth limit under `/orchestrate` with no `Agent` tool and strands the PR at `REREVIEW-REQUIRED` (forge#3391). The router runs `work-on:remediate`, which classifies a CI-gate refusal as FIXABLE, swaps `workflow:remediating` (or a legacy `needs-human`) for `workflow:in-review`, fixes the failing jobs on the PR branch, re-runs the quality gate and re-reviews the new head (scoped by /review-pr: full, delta or carried forward), and auto-lands through the same CI gate. <!-- forge#3191 -->
|
|
566
587
|
|
|
@@ -574,27 +595,19 @@ gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state'
|
|
|
574
595
|
In-PR fix round did not land; the remaining CONFIRMED MEDIUM findings are filed as issues on the next review instead of blocking the merge." # allowlist:check-command-side-effects
|
|
575
596
|
```
|
|
576
597
|
|
|
577
|
-
Persisted loop bounds for the cases above — count first, then act (`{BOUND}` is `STALE_REREVIEW`, `CI_REMEDIATION`, `INPR_REMEDIATION` or `BASESYNC_REMEDIATION`). This phase posts only the `STALE_REREVIEW`
|
|
598
|
+
Persisted loop bounds for the cases above — count first, then act (`{BOUND}` is `STALE_REREVIEW`, `DEGRADED_REREVIEW`, `CI_REMEDIATION`, `INPR_REMEDIATION` or `BASESYNC_REMEDIATION`). This phase posts only the `STALE_REREVIEW` and `DEGRADED_REREVIEW` markers; `CI_REMEDIATION`, `INPR_REMEDIATION` and `BASESYNC_REMEDIATION` are posted by the router in Phase 4R, immediately before it invokes remediation (forge#3398):
|
|
578
599
|
```bash
|
|
579
600
|
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] bound check only"; }
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
--jq '[.[] | select((.body | contains("FORGE:STALE_REREVIEW:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
583
|
-
elif [ "{BOUND}" = "INPR_REMEDIATION" ]; then
|
|
584
|
-
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
585
|
-
--jq '[.[] | select((.body | contains("FORGE:INPR_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
586
|
-
elif [ "{BOUND}" = "BASESYNC_REMEDIATION" ]; then
|
|
587
|
-
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
588
|
-
--jq '[.[] | select((.body | contains("FORGE:BASESYNC_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
589
|
-
else
|
|
590
|
-
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
591
|
-
--jq '[.[] | select((.body | contains("FORGE:CI_REMEDIATION:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
592
|
-
fi
|
|
601
|
+
BOUND_COUNT=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate \
|
|
602
|
+
--jq '[.[] | select((.body | contains("FORGE:{BOUND}:")) and (.body | contains("pr={PR_NUMBER}")))] | length' 2>/dev/null | awk '{s+=$(1)} END {print s+0}')
|
|
593
603
|
if [ "$BOUND_COUNT" -ge 1 ]; then
|
|
594
604
|
echo "BOUND_EXHAUSTED: {BOUND} already used for PR #{PR_NUMBER}"
|
|
595
605
|
elif [ "{BOUND}" = "STALE_REREVIEW" ] && [ "${DRY_RUN:-false}" != "true" ]; then
|
|
596
606
|
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:STALE_REREVIEW: pr={PR_NUMBER} -->
|
|
597
607
|
Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head once." 2>/dev/null || true
|
|
608
|
+
elif [ "{BOUND}" = "DEGRADED_REREVIEW" ] && [ "${DRY_RUN:-false}" != "true" ]; then
|
|
609
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DEGRADED_REREVIEW: pr={PR_NUMBER} -->
|
|
610
|
+
Degraded-gate review on PR #{PR_NUMBER}: re-reviewing once so the review names each degraded check." 2>/dev/null || true
|
|
598
611
|
fi
|
|
599
612
|
```
|
|
600
613
|
`BOUND_EXHAUSTED` → take the "already ≥ 1" branch of that case.
|
|
@@ -603,7 +616,7 @@ Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head on
|
|
|
603
616
|
- **Bound unused**: run the remediation-pending handoff block above (set `workflow:remediating`, no `needs-human`; a pure base sync needs no human). Then return `REVIEW_RESULT: status: NEXT` with `next: remediate` and `remediation: base-sync`. The router posts the bound marker (Phase 4R) and runs `work-on:remediate`, which merges the base in, re-runs the quality gate and re-reviews the new head. Never invoke `work-on:remediate` from this phase (same reason as the ci-gate case).
|
|
604
617
|
- **Bound already used** (the sync ran and the PR still conflicts, or the sync could not resolve a hunk): do not sync again. Remove `workflow:remediating`, label the issue `needs-human` (a genuine human decision now) and return `REVIEW_RESULT: status: BLOCKED` with blocker "base conflict persists after base-sync remediation". List the conflicting files in the issue comment you post, taken from the remediation's `FORGE:BASESYNC_FAILED` comment (the `git diff --name-only --diff-filter=U` list) when present.
|
|
605
618
|
|
|
606
|
-
- PR NOT MERGED (and not a phase-trail, auto-merge-gate, stale-review, ci-gate, in-pr-fix or base-conflict BLOCKED above) → attempt manual merge, **only if the PR head is still the commit named in the latest `<!-- FORGE:REVIEW -->` APPROVED verdict** (otherwise re-invoke Phase R3 instead, as for "stale review") **and only after the same CI gate**:
|
|
619
|
+
- PR NOT MERGED (and not a phase-trail, auto-merge-gate, stale-review, degraded-gate-unreviewed, ci-gate, in-pr-fix or base-conflict BLOCKED above) → attempt manual merge, **only if the PR head is still the commit named in the latest `<!-- FORGE:REVIEW -->` APPROVED verdict** (otherwise re-invoke Phase R3 instead, as for "stale review") **and only after the same CI gate**:
|
|
607
620
|
```bash
|
|
608
621
|
# CI gate (MANDATORY before any autonomous merge): merge only when every check on the PR is
|
|
609
622
|
# green. Field test: PRs merged to staging with checks pending or red (#3165), because branch
|
|
@@ -617,7 +630,7 @@ Stale review on PR #{PR_NUMBER}: quality-gating and re-reviewing the new head on
|
|
|
617
630
|
else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
|
|
618
631
|
echo "$CI_GATE_OUT"
|
|
619
632
|
GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
|
|
620
|
-
# rc 3 = CI still running: re-run this block (up to 3 more times) before treating
|
|
633
|
+
# rc 3 = CI still running, rc 4 = GitHub rate-limited/unavailable (forge#4018): both are waits, not failures; re-run this block (up to 3 more times) before treating either as a failure.
|
|
621
634
|
# Note-disposition gate (same rule as review-pr.md Phase 8): findings without a recorded
|
|
622
635
|
# §6B.5 disposition must not merge, because that step is what bounds the review-finding cascade.
|
|
623
636
|
DISPO_JSON=$(gh api --paginate "repos/{GH_REPO}/issues/{PR_NUMBER}/comments" 2>/dev/null) || DISPO_JSON=""
|