forgedock 1.16.2 → 1.18.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.
Files changed (114) hide show
  1. package/bin/engine/outcome-codes.mjs +114 -0
  2. package/bin/engine/phases.mjs +211 -100
  3. package/bin/engine/projector.mjs +48 -1
  4. package/bin/engine/runlog.mjs +20 -5
  5. package/bin/engine/telemetry-seam.mjs +62 -0
  6. package/bin/engine/telemetry-seam.test.mjs +60 -0
  7. package/bin/engine/telemetry.mjs +148 -0
  8. package/bin/engine/telemetry.test.mjs +120 -0
  9. package/bin/engine-cli.mjs +17 -1
  10. package/bin/engine.mjs +227 -48
  11. package/bin/forgedock.mjs +8 -2
  12. package/bin/hooks/interactive-engine.mjs +10 -0
  13. package/bin/hooks/pre-tool-use.mjs +220 -0
  14. package/bin/runner.mjs +70 -4
  15. package/bin/stats/aggregate.mjs +231 -0
  16. package/bin/stats/cache.mjs +40 -0
  17. package/bin/stats/github-source.mjs +211 -0
  18. package/bin/stats/index.mjs +154 -0
  19. package/bin/stats/local-source.mjs +79 -0
  20. package/bin/stats/package.json +6 -0
  21. package/bin/stats/render.mjs +82 -0
  22. package/bin/stats/stats.test.mjs +312 -0
  23. package/commands/cleanup.md +90 -2
  24. package/commands/deploy-info.md +8 -4
  25. package/commands/deploy-pr.md +64 -0
  26. package/commands/fix-ci.md +20 -6
  27. package/commands/milestone.md +34 -17
  28. package/commands/orchestrate/phase-1-resolve.md +4 -1
  29. package/commands/orchestrate/phase-4-execution.md +28 -6
  30. package/commands/orchestrate/phase-5-cleanup.md +18 -2
  31. package/commands/pipeline-health/phase-1-context.md +2 -0
  32. package/commands/pipeline-health/phase-2-metrics.md +12 -5
  33. package/commands/pipeline-status.md +21 -1
  34. package/commands/review-pr-agents/protocols.md +1 -0
  35. package/commands/review-pr-agents.md +10 -2
  36. package/commands/review-pr-staging.md +196 -496
  37. package/commands/review-pr.md +512 -3134
  38. package/commands/test-gate.md +25 -10
  39. package/commands/work-on/build/architect.md +13 -1
  40. package/commands/work-on/build/implement.md +28 -6
  41. package/commands/work-on/build/validate.md +5 -2
  42. package/commands/work-on/build.md +197 -145
  43. package/commands/work-on/close.md +66 -20
  44. package/commands/work-on/decompose.md +113 -34
  45. package/commands/work-on/investigate.md +15 -14
  46. package/commands/work-on/remediate.md +49 -19
  47. package/commands/work-on/review.md +7 -5
  48. package/commands/work-on.md +1 -2
  49. package/docs/CONFIG.md +76 -24
  50. package/docs/OUTCOME-CODES.md +53 -0
  51. package/docs/RELEASE-NOTES.md +24 -0
  52. package/docs/STATS.md +63 -0
  53. package/docs/TELEMETRY.md +65 -0
  54. package/docs/WORK-ON-RUNTIME.md +19 -0
  55. package/docs/site/command-reference.md +1 -1
  56. package/docs/spec/forge-protocol-v1.md +2 -2
  57. package/docs/spec/label-state-machine.md +1 -1
  58. package/package.json +3 -2
  59. package/packages/protocol/src/phases.js +5 -5
  60. package/scripts/builder-resume-match.test.sh +58 -0
  61. package/scripts/cap-findings.sh +110 -0
  62. package/scripts/cap-findings.test.sh +130 -0
  63. package/scripts/check-spec-bash.sh +5 -1
  64. package/scripts/check-spec-bash.test.sh +20 -0
  65. package/scripts/check-spec-markers.sh +2 -0
  66. package/scripts/classify-finding.sh +65 -16
  67. package/scripts/classify-finding.test.sh +43 -1
  68. package/scripts/classify-grep-exit.sh +20 -0
  69. package/scripts/classify-grep-exit.test.sh +46 -0
  70. package/scripts/close-parent-c3.test.sh +123 -0
  71. package/scripts/cost-report.sh +146 -0
  72. package/scripts/cost-report.test.sh +80 -0
  73. package/scripts/diff-size.sh +97 -3
  74. package/scripts/diff-size.test.sh +59 -2
  75. package/scripts/doctor-pipeline-state.sh +43 -0
  76. package/scripts/file-deferred-followups.sh +159 -0
  77. package/scripts/file-deferred-followups.test.sh +106 -0
  78. package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
  79. package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
  80. package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
  81. package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
  82. package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
  83. package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
  84. package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
  85. package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
  86. package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
  87. package/scripts/forge-root.test.sh +18 -11
  88. package/scripts/lib/fence-bootstrap.sh +4 -3
  89. package/scripts/lib/pr-issue-number.sh +18 -0
  90. package/scripts/lib/review-template-resolve.sh +41 -0
  91. package/scripts/lib/telemetry.sh +168 -0
  92. package/scripts/publish-staging-sync.test.sh +48 -0
  93. package/scripts/remediate-liveness.sh +42 -6
  94. package/scripts/resolve-deploy-gate.sh +32 -6
  95. package/scripts/resolve-deploy-gate.test.sh +42 -1
  96. package/scripts/review-acceptance-gate.sh +16 -7
  97. package/scripts/review-acceptance-gate.test.sh +13 -0
  98. package/scripts/review-finalize.sh +1427 -0
  99. package/scripts/review-finalize.test.sh +833 -0
  100. package/scripts/review-pr-delta-wiring.test.sh +19 -72
  101. package/scripts/review-pr-domain-map.test.sh +23 -13
  102. package/scripts/review-pr-gate2.test.sh +24 -65
  103. package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
  104. package/scripts/review-pr-staging-scope.test.sh +56 -80
  105. package/scripts/review-prepare.sh +553 -0
  106. package/scripts/review-prepare.test.sh +288 -0
  107. package/scripts/review-roster.sh +186 -0
  108. package/scripts/review-roster.test.sh +185 -0
  109. package/scripts/run-shell-tests.sh +3 -0
  110. package/scripts/telemetry.test.sh +106 -0
  111. package/scripts/trusted-comments.test.sh +9 -2
  112. package/scripts/wait-ci-green.sh +8 -1
  113. package/scripts/wait-ci-green.test.sh +3 -3
  114. package/templates/forge.yaml.minimal +9 -0
@@ -11,7 +11,9 @@
11
11
  # A base is the deploy gate when it is empty/`null`, the literal `main` (hard
12
12
  # floor), the repository's GitHub default branch (`default_branch`), or the
13
13
  # `branches.default` of the default branch's COMMITTED forge.yaml
14
- # (`origin/<default>:forge.yaml`, falling back to the contents API) or of an
14
+ # (`refs/remotes/origin/<default>:forge.yaml`, falling back to the contents API;
15
+ # if neither source is readable the result is `true`, never `false`; a source that
16
+ # cleanly reports forge.yaml absent is "no config" and falls through) or of an
15
17
  # explicit trusted config (FORGE_TRUSTED_CONFIG, orchestrator-supplied).
16
18
  #
17
19
  # It NEVER reads forge.yaml from $PWD: the PR worktree is author-controlled, so
@@ -34,13 +36,37 @@ GH_DEFAULT=$(gh api "repos/${REPO}" --jq .default_branch 2>/dev/null || echo "")
34
36
 
35
37
  _cfg_default() { yq '.branches.default // ""' 2>/dev/null || true; }
36
38
 
39
+ # Parse stdin's branches.default. Returns non-zero when yq is missing or the
40
+ # parse fails, so "config unreadable" is distinguishable from "no branches.default".
41
+ _cfg_parse() { command -v yq >/dev/null 2>&1 && yq '.branches.default // ""' 2>/dev/null; }
42
+
37
43
  CFG_DEFAULT=""
38
- # Committed default-branch config only (a ref, never the working tree).
39
- CFG_DEFAULT=$(git show "origin/${GH_DEFAULT}:forge.yaml" 2>/dev/null | _cfg_default)
40
- if [ -z "$CFG_DEFAULT" ]; then
41
- CFG_DEFAULT=$(gh api "repos/${REPO}/contents/forge.yaml?ref=${GH_DEFAULT}" --jq .content 2>/dev/null \
42
- | base64 -d 2>/dev/null | _cfg_default)
44
+ READ_OK=""
45
+ # Committed default-branch config only (a ref, never the working tree). The
46
+ # fully-qualified ref cannot be shadowed by a local tag/branch named origin/<default>.
47
+ # A present ref with no forge.yaml at that path is a clean "no config" read (READ_OK,
48
+ # empty default); an unfetched ref, missing yq or parse error stays unreadable.
49
+ CFG_REF="refs/remotes/origin/${GH_DEFAULT}"
50
+ if git rev-parse -q --verify "${CFG_REF}^{commit}" >/dev/null 2>&1; then
51
+ if git cat-file -e "${CFG_REF}:forge.yaml" 2>/dev/null; then
52
+ CFG_RAW=$(git show "${CFG_REF}:forge.yaml" 2>/dev/null) && [ -n "$CFG_RAW" ] \
53
+ && CFG_DEFAULT=$(printf '%s\n' "$CFG_RAW" | _cfg_parse) && READ_OK=1
54
+ else
55
+ READ_OK=1
56
+ fi
57
+ fi
58
+ if [ -z "$READ_OK" ]; then
59
+ CFG_DEFAULT=""
60
+ API_OUT=$(gh api "repos/${REPO}/contents/forge.yaml?ref=${GH_DEFAULT}" --jq .content 2>&1); API_RC=$?
61
+ if [ "$API_RC" -eq 0 ]; then
62
+ CFG_RAW=$(printf '%s' "$API_OUT" | base64 -d 2>/dev/null) && [ -n "$CFG_RAW" ] \
63
+ && CFG_DEFAULT=$(printf '%s\n' "$CFG_RAW" | _cfg_parse) && READ_OK=1
64
+ elif printf '%s' "$API_OUT" | grep -q 'HTTP 404'; then
65
+ READ_OK=1 # API says the file does not exist: clean "no config", not a read failure
66
+ fi
43
67
  fi
68
+ # Config unreadable from every source (no yq, ref unfetched, API failing): fail closed.
69
+ [ -n "$READ_OK" ] || emit true
44
70
  [ -n "$CFG_DEFAULT" ] && [ "$CFG_DEFAULT" != "null" ] && [ "$BASE" = "$CFG_DEFAULT" ] && emit true
45
71
 
46
72
  if [ -n "${FORGE_TRUSTED_CONFIG:-}" ] && [ -f "$FORGE_TRUSTED_CONFIG" ]; then
@@ -9,7 +9,7 @@ TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
9
9
  ok() { PASS=$((PASS+1)); }
10
10
  bad() { FAILN=$((FAILN+1)); echo "FAIL: $1"; }
11
11
 
12
- command -v yq >/dev/null 2>&1 || { echo "SKIP: yq not installed"; exit 0; }
12
+ HAVE_YQ=1; command -v yq >/dev/null 2>&1 || HAVE_YQ=0
13
13
 
14
14
  # Stub gh: GH_DEFAULT_STUB drives `repos/<r> --jq .default_branch`; contents API unused (fails).
15
15
  mkdir -p "$TMP/bin"
@@ -17,6 +17,7 @@ cat > "$TMP/bin/gh" <<'EOF'
17
17
  #!/usr/bin/env bash
18
18
  case "$*" in
19
19
  *"--jq .default_branch"*) [ -n "${GH_FAIL:-}" ] && exit 1; printf '%s\n' "${GH_DEFAULT_STUB:-}" ;;
20
+ *"contents/forge.yaml"*) [ -n "${GH_CONTENTS_404:-}" ] && { echo "gh: Not Found (HTTP 404)" >&2; exit 1; }; exit 1 ;;
20
21
  *) exit 1 ;;
21
22
  esac
22
23
  EOF
@@ -40,6 +41,7 @@ run() { # base default_stub [env...]
40
41
  }
41
42
  expect() { [ "$1" = "$2" ] && ok || bad "$3: got '$1' want '$2'"; }
42
43
 
44
+ if [ "$HAVE_YQ" = 1 ]; then
43
45
  expect "$(run main trunk)" true "main with worktree release"
44
46
  expect "$(run release trunk)" false "worktree-declared branch is not a gate"
45
47
  expect "$(run staging trunk)" false "staging is fast lane"
@@ -55,5 +57,44 @@ printf 'branches:\n default: prod\n' > "$TMP/trusted.yaml"
55
57
  expect "$(run prod trunk FORGE_TRUSTED_CONFIG="$TMP/trusted.yaml")" true "trusted config default"
56
58
  expect "$(run prod trunk)" false "prod without trusted config"
57
59
 
60
+
61
+ # Shadowing: a local tag named origin/trunk carrying a config without `develop`
62
+ # must not displace the committed refs/remotes/origin/trunk config.
63
+ printf 'branches:\n default: other\n' > "$REPO/forge.yaml"
64
+ git -C "$REPO" add forge.yaml; git -C "$REPO" commit -qm shadow
65
+ git -C "$REPO" tag origin/trunk
66
+ expect "$(run develop trunk)" true "shadow tag origin/trunk does not displace committed config"
67
+ expect "$(run other trunk)" false "shadow tag config is not honoured"
68
+ git -C "$REPO" tag -d origin/trunk >/dev/null
69
+ fi
70
+
71
+ # Config read fail: no origin ref and failing contents API -> true (fail closed).
72
+ NOREF="$TMP/noref"; git init -q "$NOREF"
73
+ run_in() { # dir base [env...]
74
+ local dir="$1" base="$2"; shift 2
75
+ (cd "$dir" && env PATH="$TMP/bin:$PATH" GH_DEFAULT_STUB=trunk "$@" bash "$S" "$base" owner/repo)
76
+ }
77
+ expect "$(run_in "$NOREF" staging)" true "config read fail (no origin ref, API failing) fails closed"
78
+
79
+ # Missing forge.yaml is a clean "no config" read, not a read failure (falls through to false).
80
+ NOCFG="$TMP/nocfg.git"; NOCFGW="$TMP/nocfgw"
81
+ git init -q --bare "$NOCFG"; git init -q "$NOCFGW"
82
+ git -C "$NOCFGW" config user.email t@t; git -C "$NOCFGW" config user.name t
83
+ echo x > "$NOCFGW/README"; git -C "$NOCFGW" add README; git -C "$NOCFGW" commit -qm init
84
+ git -C "$NOCFGW" branch -M trunk; git -C "$NOCFGW" remote add origin "$NOCFG"
85
+ git -C "$NOCFGW" push -q origin trunk 2>/dev/null; git -C "$NOCFGW" fetch -q origin
86
+ expect "$(run_in "$NOCFGW" staging)" false "forge.yaml absent on default branch ref is no-config (fast lane)"
87
+ expect "$(run_in "$NOCFGW" main)" true "main stays strict with no config"
88
+ expect "$(run_in "$NOCFGW" trunk)" true "github default stays strict with no config"
89
+ # API 404 (ref not fetched, file absent) is also a clean no-config read; other API failures stay closed.
90
+ expect "$(run_in "$NOREF" staging GH_CONTENTS_404=1)" false "contents API 404 is no-config"
91
+ expect "$(run_in "$NOREF" staging)" true "non-404 contents API failure fails closed"
92
+
93
+ # Config unreadable: yq unusable (a failing stub shadows any real yq; portable to Git Bash, unlike a stripped PATH) even though the ref is readable.
94
+ NOYQ="$TMP/noyqbin"; mkdir -p "$NOYQ"
95
+ printf '#!/usr/bin/env bash\nexit 127\n' > "$NOYQ/yq"; chmod +x "$NOYQ/yq"
96
+ cp "$TMP/bin/gh" "$NOYQ/gh"
97
+ expect "$(cd "$REPO" && env PATH="$NOYQ:$PATH" GH_DEFAULT_STUB=trunk bash "$S" staging owner/repo)" true "config unreadable (yq failing) fails closed"
98
+
58
99
  echo "resolve-deploy-gate.test.sh: $PASS passed, $FAILN failed"
59
100
  [ "$FAILN" -eq 0 ]
@@ -4,13 +4,17 @@
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  #
6
6
  # Usage: review-acceptance-gate.sh <issue> <gh_repo> "<gh_flag>" <worktree_path> (DRY_RUN=1 skips gh writes)
7
- # Evaluates the ACCEPTANCE_CHECK lines the build gate (B6.5) deferred: `phase=review` checks, plus untagged legacy
8
- # command/behavior checks on a `gh pr` target. Uses its own FORGE:ACCEPTANCE_GATE_REVIEW marker, never the build
7
+ # Evaluates the ACCEPTANCE_CHECK lines the build gate (B6.5) deferred: `phase=review` checks, plus command/behavior
8
+ # checks on a `gh pr` target (B6.5's legacy deferral ignores any phase= tag, so a tagged phase=build one is selected too). Uses its own FORGE:ACCEPTANCE_GATE_REVIEW marker, never the build
9
9
  # gate's FORGE:ACCEPTANCE_GATE header or :PASSED/:FAILED/:BLOCKED sentinels (the engine reads those).
10
10
  # Malformed checks (forge#3694 parity with B6.5: 4+ backslash run, `bash -n` parse failure, grep usage error) are skipped and listed,
11
11
  # never a FAIL and never needs-human. With zero verified checks and some malformed, a non-blocking notice is posted (no PASSED marker).
12
12
  # Exit: 0 passed / no-op / notice / already passed for this head, 1 a deferred check failed (issue gets needs-human).
13
13
  set -uo pipefail
14
+ # Telemetry (forge#3834): best-effort decision record; never changes stdout, stderr or the exit code.
15
+ _TEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/telemetry.sh"; [ -f "$_TEL" ] && . "$_TEL" 2>/dev/null
16
+ type forge_emit >/dev/null 2>&1 || forge_emit() { :; }
17
+ [ -z "${DRY_RUN:-}" ] || FORGE_TELEMETRY=off
14
18
 
15
19
  NUMBER="${1:?issue number}"; GH_REPO="${2:?repo}"; GH_FLAG="${3:-}"; WT="${4:-.}"
16
20
  cd "$WT" || exit 2
@@ -42,6 +46,7 @@ if printf '%s' "$LAST" | grep -qF "<!-- FORGE:ACCEPTANCE_GATE_REVIEW:PASSED:${HE
42
46
  fi
43
47
 
44
48
  CHECKS=$(printf '%s\n' "$TRUSTED_INV" | jq -r '.' 2>/dev/null | grep "^ACCEPTANCE_CHECK:")
49
+ GREP_CLS=$(forge_resolve classify-grep-exit.sh) # unresolved = grep exit>=2 stays a real FAIL (fail closed, forge#3802)
45
50
  GATE_PASS=true; FAILED=""; VERIFIED_COUNT=0; MALFORMED_CHECKS=""; RESULTS=""
46
51
  while IFS= read -r check_line; do
47
52
  [ -z "$check_line" ] && continue
@@ -57,12 +62,12 @@ while IFS= read -r check_line; do
57
62
  [ "$PHASE" = "review" ] || PHASE=build
58
63
  case "$TYPE" in command|behavior) ;; *) [ "$PHASE" = "review" ] || continue ;; esac
59
64
  if [ "$PHASE" != "review" ]; then
60
- printf '%s\n' "$check_line" | grep -qE ' phase=(build|review)( |$)' && continue
61
65
  case "$TARGET" in *'gh pr'*) ;; *) continue ;; esac
62
66
  fi
63
67
  # malformed-check (forge#3694): static screen only parses (bash -n), never executes.
64
68
  MALFORMED=""
65
69
  { [ "$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)"; }
70
+ { [ "$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
66
71
  if [ -n "$MALFORMED" ]; then MALFORMED_CHECKS="${MALFORMED_CHECKS}\n- **$ID** ($DESC): $MALFORMED"; continue; fi
67
72
  RESULT="PASS"; DETAIL=""
68
73
  case "$TYPE" in
@@ -72,12 +77,14 @@ while IFS= read -r check_line; do
72
77
  command|behavior)
73
78
  if [ "$MATCHER" = "exit_0" ]; then
74
79
  ERR=$(eval "$TARGET" 2>&1 >/dev/null); RC=$?
75
- # grep exit >= 2 is a usage error, not a verdict; a missing/unreadable operand also exits 2 but stays a real FAIL.
76
- [ "$RC" -ge 2 ] && case "$TARGET" in grep\ *|egrep\ *|fgrep\ *) printf '%s' "$ERR" | grep -qE 'No such file or directory|Permission denied|Is a directory' || MALFORMED="grep usage error (exit $RC)";; esac
80
+ # grep exit >= 2 is a usage error, not a verdict; only a positively identified usage error (classify-grep-exit.sh) is malformed; a missing operand stays a real FAIL even with stderr suppressed.
81
+ [ "$RC" -ge 2 ] && MALFORMED=$(bash "$GREP_CLS" "$RC" "$ERR" "$TARGET" 2>/dev/null)
82
+ [ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && printf '%s' "$ERR" | grep -qF 'command not found' && MALFORMED="command not found (rc 127): the target is not an executable command"
77
83
  [ "$RC" -eq 0 ] || [ -n "$MALFORMED" ] || { RESULT="FAIL"; DETAIL="command exited non-zero: $TARGET"; }
78
84
  else
79
- OUTPUT=$(eval "$TARGET" 2>&1); RC=$?; ERR="$OUTPUT"
80
- [ "$RC" -ge 2 ] && case "$TARGET" in grep\ *|egrep\ *|fgrep\ *) printf '%s' "$ERR" | grep -qE 'No such file or directory|Permission denied|Is a directory' || MALFORMED="grep usage error (exit $RC)";; esac
85
+ 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
86
+ [ "$RC" -ge 2 ] && MALFORMED=$(bash "$GREP_CLS" "$RC" "$ERR" "$TARGET" 2>/dev/null)
87
+ [ "$RC" -eq 127 ] && [ -z "$MALFORMED" ] && printf '%s' "$ERR" | grep -qF 'command not found' && MALFORMED="command not found (rc 127): the target is not an executable command"
81
88
  if [ -n "$MALFORMED" ]; then :
82
89
  else
83
90
  printf '%s\n' "$OUTPUT" | grep -qE "$MATCHER" || printf '%s\n' "$OUTPUT" | grep -qF -- "$MATCHER" \
@@ -95,6 +102,8 @@ done <<< "$CHECKS"
95
102
  MALFORMED_SECTION=""
96
103
  [ -n "$MALFORMED_CHECKS" ] && MALFORMED_SECTION=$(printf '\n**malformed-check (not counted as failures; regenerate the acceptance spec by re-running investigate)**:%b\n' "$MALFORMED_CHECKS")
97
104
 
105
+ _nf=$(printf '%b' "$FAILED" | grep -c '^- ' || true); _nm=$(printf '%b' "$MALFORMED_CHECKS" | grep -c '^- ' || true)
106
+ FORGE_REPO="${FORGE_REPO:-$GH_REPO}" FORGE_ISSUE="${FORGE_ISSUE:-$NUMBER}" forge_emit acceptance_gate phase=review verified="$VERIFIED_COUNT" failed="${_nf:-0}" malformed="${_nm:-0}" deferred=$((VERIFIED_COUNT + ${_nm:-0}))
98
107
  if [ "$VERIFIED_COUNT" -eq 0 ] && [ -z "$MALFORMED_CHECKS" ]; then
99
108
  echo "No deferred acceptance checks; R2.5 is a no-op."
100
109
  elif [ "$VERIFIED_COUNT" -eq 0 ]; then
@@ -75,6 +75,9 @@ inv "$(chk ac-1 false)"; rc=$(run); eq "real fail: rc 1" "$rc" 1
75
75
  grep -q 'needs-human' "$TMP/out" && ok "real fail: needs-human added" || bad "real fail: no needs-human"
76
76
  inv "$(chk ac-1 'grep -q nomatch f.txt')"; rc=$(run); eq "grep exit 1 is a real fail" "$rc" 1
77
77
  inv "$(chk ac-1 'grep -q x nofile.txt')"; rc=$(run); eq "grep missing operand file stays a real fail" "$rc" 1
78
+ inv "$(chk ac-1 'grep -q x nofile.txt 2>/dev/null')"; rc=$(run); eq "stderr-suppressed missing operand is a real fail (forge#3802)" "$rc" 1
79
+ grep -q ':PASSED:' "$TMP/out" && bad "stderr-suppressed missing operand wrote PASSED" || ok "stderr-suppressed missing operand: no PASSED marker"
80
+ inv "$(chk ac-1 'grep -sq x nofile.txt')"; rc=$(run); eq "grep -s missing operand is a real fail (forge#3802)" "$rc" 1
78
81
 
79
82
  for t in "echo 'unbalanced" 'echo a\\\\\\\\b' "grep -E '[' f.txt"; do
80
83
  inv "$(chk ac-7 "$t")"; rc=$(run); eq "malformed skipped, rc 0: $t" "$rc" 0
@@ -87,4 +90,14 @@ inv "$(chk ac-1 true)" "$(chk ac-7 "echo 'unbalanced")"; rc=$(run); eq "mixed pa
87
90
  grep -q ':PASSED:' "$TMP/out" && grep -qi 'malformed' "$TMP/out" && ok "mixed: PASSED with malformed listed" || bad "mixed: wrong output"
88
91
  inv "$(chk ac-1 false)" "$(chk ac-7 "echo 'unbalanced")"; rc=$(run); eq "mixed fail+malformed: fails closed" "$rc" 1
89
92
 
93
+ # Selection parity with B6.5 (forge#3809): non-review checks are selected by the `gh pr` target only, never by a phase= text match.
94
+ bt() { printf 'ACCEPTANCE_CHECK: id=%s type=command target="%s" matcher="exit_0" phase=%s description=%s' "$1" "$2" "$3" "$4"; }
95
+ inv "$(bt ac-1 'gh pr view 1 && false' build d)"; rc=$(run); eq "phase=build gh pr check is evaluated (fails)" "$rc" 1
96
+ inv "$(bt ac-1 'gh pr view 1 && true' build d)"; rc=$(run)
97
+ grep -q ':PASSED:' "$TMP/out" && ok "phase=build gh pr check counted and passes" || bad "phase=build gh pr check not counted"
98
+ inv "$(bt ac-1 'false' build d)"; rc=$(run); eq "phase=build non-gh-pr check is not selected" "$rc" 0
99
+ inv "$(bt ac-1 'gh pr view 1 && false' build 'x phase=review y')"; rc=$(run); eq "phase=review in description does not change selection" "$rc" 1
100
+ inv "$(bt ac-1 'false' build 'x phase=review y')"; rc=$(run); eq "phase=review in description does not select a non-gh-pr check" "$rc" 0
101
+ inv 'ACCEPTANCE_CHECK: id=ac-1 type=command target="gh pr view 1 && false" matcher="exit_0" description=untagged'; rc=$(run); eq "untagged gh pr check is evaluated" "$rc" 1
102
+
90
103
  echo "$PASS passed, $FAILN failed"; [ "$FAILN" -eq 0 ]