forgedock 1.18.0 → 1.20.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 (135) hide show
  1. package/README.md +2 -0
  2. package/bin/engine/outcome-codes.mjs +13 -0
  3. package/bin/engine/phases.mjs +54 -4
  4. package/bin/engine/runlog.mjs +10 -2
  5. package/bin/engine/telemetry-seam.mjs +3 -0
  6. package/bin/engine/telemetry.test.mjs +3 -2
  7. package/bin/engine-cli.mjs +32 -2
  8. package/bin/engine.mjs +187 -10
  9. package/bin/runner.mjs +89 -13
  10. package/bin/stats/aggregate.mjs +7 -3
  11. package/bin/stats/cache.mjs +7 -3
  12. package/bin/stats/github-source.mjs +136 -28
  13. package/bin/stats/index.mjs +46 -9
  14. package/bin/stats/render.mjs +1 -1
  15. package/bin/stats/stats.test.mjs +201 -40
  16. package/commands/orchestrate/phase-4-execution.md +1 -1
  17. package/commands/review-pr-agents/protocols.md +19 -0
  18. package/commands/review-pr-staging.md +4 -4
  19. package/commands/review-pr.md +31 -20
  20. package/commands/work-on/build/architect.md +54 -1
  21. package/commands/work-on/build/implement.md +28 -1
  22. package/commands/work-on/build/validate.md +321 -29
  23. package/commands/work-on/build.md +184 -17
  24. package/commands/work-on/close.md +9 -4
  25. package/commands/work-on/investigate.md +7 -0
  26. package/commands/work-on/remediate.md +43 -17
  27. package/commands/work-on/review.md +43 -22
  28. package/commands/work-on.md +4 -4
  29. package/docs/CONFIG.md +93 -2
  30. package/docs/OUTCOME-CODES.md +5 -1
  31. package/docs/RELEASE-NOTES.md +15 -0
  32. package/docs/STATS.md +11 -4
  33. package/docs/TELEMETRY.md +5 -2
  34. package/docs/WORK-ON-RUNTIME.md +29 -3
  35. package/package.json +2 -2
  36. package/packages/protocol/src/types.js +1 -1
  37. package/scripts/acceptance-red-green.sh +90 -0
  38. package/scripts/acceptance-red-green.test.sh +74 -0
  39. package/scripts/architect-resume-match.test.sh +12 -3
  40. package/scripts/build-knowledge-index.mjs +8 -5
  41. package/scripts/builder-resume-match.test.sh +12 -3
  42. package/scripts/calibration.mjs +5 -2
  43. package/scripts/cap-findings.test.sh +2 -0
  44. package/scripts/check-contract-scope.test.sh +2 -0
  45. package/scripts/check-edge-cases.sh +143 -0
  46. package/scripts/check-edge-cases.test.sh +175 -0
  47. package/scripts/check-spec-markers.sh +4 -0
  48. package/scripts/check-test-tmp-cleanup.sh +56 -0
  49. package/scripts/check-test-tmp-cleanup.test.sh +69 -0
  50. package/scripts/ci-local/adapters/azure-pipelines.mjs +113 -0
  51. package/scripts/ci-local/adapters/bitbucket.mjs +64 -0
  52. package/scripts/ci-local/adapters/buildkite.mjs +58 -0
  53. package/scripts/ci-local/adapters/circleci.mjs +101 -0
  54. package/scripts/ci-local/adapters/common.mjs +115 -0
  55. package/scripts/ci-local/adapters/explicit.mjs +21 -0
  56. package/scripts/ci-local/adapters/github-actions.mjs +349 -0
  57. package/scripts/ci-local/adapters/gitlab-ci.mjs +184 -0
  58. package/scripts/ci-local/adapters/portability.mjs +104 -0
  59. package/scripts/ci-local/adapters/task-runners.mjs +67 -0
  60. package/scripts/ci-local/cli.mjs +50 -0
  61. package/scripts/ci-local/lib.mjs +379 -0
  62. package/scripts/ci-local/plan.mjs +186 -0
  63. package/scripts/ci-local/run.mjs +451 -0
  64. package/scripts/ci-local/scoping.mjs +531 -0
  65. package/scripts/ci-local.sh +98 -0
  66. package/scripts/ci-local.test.sh +1108 -0
  67. package/scripts/classify-finding.sh +33 -7
  68. package/scripts/classify-finding.test.sh +23 -0
  69. package/scripts/classify-test-run.sh +69 -0
  70. package/scripts/classify-test-run.test.sh +90 -0
  71. package/scripts/close-parent-c3.test.sh +2 -0
  72. package/scripts/consumer-sweep.sh +569 -0
  73. package/scripts/consumer-sweep.test.sh +357 -0
  74. package/scripts/detect-append-registries.sh +106 -0
  75. package/scripts/detect-append-registries.test.sh +64 -0
  76. package/scripts/file-deferred-followups.sh +91 -30
  77. package/scripts/file-deferred-followups.test.sh +101 -8
  78. package/scripts/fixtures/ci-local/azure/azure-pipelines.yml +11 -0
  79. package/scripts/fixtures/ci-local/bitbucket/bitbucket-pipelines.yml +12 -0
  80. package/scripts/fixtures/ci-local/buildkite/.buildkite/pipeline.yml +10 -0
  81. package/scripts/fixtures/ci-local/circleci/.circleci/config.yml +29 -0
  82. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/ci.yml +59 -0
  83. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/docs.yml +10 -0
  84. package/scripts/fixtures/ci-local/gha-basic/.github/workflows/nightly.yml +10 -0
  85. package/scripts/fixtures/ci-local/gha-basic/docs/index.md +1 -0
  86. package/scripts/fixtures/ci-local/gha-basic/src/greet.sh +2 -0
  87. package/scripts/fixtures/ci-local/gha-basic/tests/greet.test.sh +3 -0
  88. package/scripts/fixtures/ci-local/gha-dorny/.github/workflows/ci.yml +51 -0
  89. package/scripts/fixtures/ci-local/gha-dorny/backend/app.py +1 -0
  90. package/scripts/fixtures/ci-local/gha-dorny/docs/a.md +1 -0
  91. package/scripts/fixtures/ci-local/gha-dorny/frontend/app.js +1 -0
  92. package/scripts/fixtures/ci-local/gha-run/.github/workflows/ci.yml +39 -0
  93. package/scripts/fixtures/ci-local/gha-run/src/x.txt +1 -0
  94. package/scripts/fixtures/ci-local/gitlab/.gitlab-ci.yml +43 -0
  95. package/scripts/fixtures/ci-local/gitlab/docs/d.md +1 -0
  96. package/scripts/fixtures/ci-local/gitlab/src/a.txt +1 -0
  97. package/scripts/fixtures/ci-local/monorepo/.github/workflows/ci.yml +13 -0
  98. package/scripts/fixtures/ci-local/monorepo/package-lock.json +1 -0
  99. package/scripts/fixtures/ci-local/monorepo/packages/a/src/a.sh +2 -0
  100. package/scripts/fixtures/ci-local/monorepo/packages/a/tests/a.test.sh +3 -0
  101. package/scripts/fixtures/ci-local/monorepo/packages/b/src/b.sh +2 -0
  102. package/scripts/fixtures/ci-local/monorepo/packages/b/tests/b.test.sh +3 -0
  103. package/scripts/fixtures/ci-local/no-ci/Makefile +6 -0
  104. package/scripts/fixtures/ci-local/no-ci/package.json +1 -0
  105. package/scripts/fixtures/ci-local/no-ci/src/a.js +1 -0
  106. package/scripts/fixtures/ci-local/no-ci/src/b.js +1 -0
  107. package/scripts/fixtures/ci-local/no-ci/tests/a.test.js +2 -0
  108. package/scripts/fixtures/ci-local/no-ci/tests/b.test.js +2 -0
  109. package/scripts/forge-root.test.sh +1 -1
  110. package/scripts/lib/telemetry.sh +3 -0
  111. package/scripts/remediate-approved-head.test.sh +2 -0
  112. package/scripts/remediate-basesync.test.sh +1 -1
  113. package/scripts/review-acceptance-gate.sh +2 -2
  114. package/scripts/review-acceptance-gate.test.sh +20 -0
  115. package/scripts/review-delta.sh +8 -1
  116. package/scripts/review-delta.test.sh +19 -0
  117. package/scripts/review-finalize.sh +294 -53
  118. package/scripts/review-finalize.test.sh +347 -9
  119. package/scripts/review-pr-delta-wiring.test.sh +13 -0
  120. package/scripts/review-prepare.sh +5 -3
  121. package/scripts/review-prepare.test.sh +18 -2
  122. package/scripts/review-roster.sh +18 -5
  123. package/scripts/review-roster.test.sh +57 -1
  124. package/scripts/run-shell-tests.sh +5 -0
  125. package/scripts/selfreview-plan.sh +86 -0
  126. package/scripts/selfreview-plan.test.sh +90 -0
  127. package/scripts/spawn-depth-check.sh +5 -4
  128. package/scripts/spawn-depth-check.test.sh +3 -0
  129. package/scripts/telemetry.test.sh +10 -0
  130. package/scripts/transition-label.sh +26 -0
  131. package/scripts/trusted-comments.test.sh +2 -1
  132. package/scripts/verify-phase-trail.test.sh +14 -0
  133. package/scripts/wait-ci-green.sh +61 -9
  134. package/scripts/wait-ci-green.test.sh +41 -3
  135. package/scripts/with-run-tmpdir.sh +20 -0
@@ -125,8 +125,24 @@ eq "auth score" '.scores.AUTH > 0' true; eq "billing score" '.scores.BILLING > 0
125
125
  eq "security baseline" .scores.SECURITY 1
126
126
  eq "risk FINANCIAL" '.risk_signals | index("FINANCIAL") != null' true
127
127
  eq "slice auth file exists" '.slices.auth.diff_file | length > 0' true
128
- if grep -q "auth.py" "$WORK/out/slice-auth.diff" 2>/dev/null; then ok; else bad "auth slice should hold the auth.py diff"; fi
129
- if grep -q "billing.py" "$WORK/out/slice-auth.diff" 2>/dev/null; then bad "auth slice leaked the billing diff"; else ok; fi
128
+ AUTH_SLICE=$(jqv '.slices.auth.diff_file')
129
+ eq "auth slice name is keyed by head SHA" '.slices.auth.diff_file | endswith("/slice-'"$(printf '%s' "$SHA_B" | cut -c1-7)"'-auth.diff")' true
130
+ if grep -q "auth.py" "$AUTH_SLICE" 2>/dev/null; then ok; else bad "auth slice should hold the auth.py diff"; fi
131
+ if grep -q "billing.py" "$AUTH_SLICE" 2>/dev/null; then bad "auth slice leaked the billing diff"; else ok; fi
132
+ if [ -z "$(ls -A "$WORK"/out/.slice-* 2>/dev/null)" ]; then ok; else bad "temp slice files left behind"; fi
133
+ # SHA isolation: a re-run at a different head SHA must not overwrite the earlier head's slice
134
+ FIRST_SUM=$(cksum < "$AUTH_SLICE")
135
+ setup feat/x staging "services/api/auth.py
136
+ services/api/billing.py" "$DIFF_AUTH_BILL
137
+ +changed-for-head-A" "$SHA_A"
138
+ prep "$ROOT"
139
+ AUTH_SLICE_A=$(jqv '.slices.auth.diff_file')
140
+ if [ -n "$AUTH_SLICE_A" ] && [ "$AUTH_SLICE_A" != "$AUTH_SLICE" ] && [ -f "$AUTH_SLICE" ] && [ -f "$AUTH_SLICE_A" ]; then ok; else bad "different head SHA must produce a distinct slice file ($AUTH_SLICE_A vs $AUTH_SLICE)"; fi
141
+ if [ "$(cksum < "$AUTH_SLICE")" = "$FIRST_SUM" ]; then ok; else bad "earlier head's slice was overwritten by a re-review at another SHA"; fi
142
+ if [ -z "$(ls -A "$WORK"/out/.slice-* 2>/dev/null)" ]; then ok; else bad "temp slice files left behind after re-run"; fi
143
+ setup feat/x staging "services/api/auth.py
144
+ services/api/billing.py" "$DIFF_AUTH_BILL"
145
+ prep "$ROOT"
130
146
  eq "auth slice paths" '.slices.auth.paths | join(" ")' "services/api/auth.py"
131
147
  eq "security slice is the full diff" '.slices.security.paths | length' 2
132
148
 
@@ -17,7 +17,8 @@
17
17
  #
18
18
  # Tiers, evaluated in this order:
19
19
  # full THOROUGH or milestone->staging: Security plus every domain with a score > 0.
20
- # fix-pr the PR closes a `review-finding` issue: Security, plus the source finding's domain when it was HIGH+.
20
+ # fix-pr the PR closes a `review-finding` issue AND has <= 150 changed lines with a known size: Security, plus the
21
+ # source finding's domain when it was HIGH+. Larger or unknown-size fix PRs fall to the size ladder below.
21
22
  # small <= 60 changed lines, or one file of <= 150 lines: Security only.
22
23
  # medium 61-150 lines: Security plus the score-escalation table (no top-2 baseline).
23
24
  # standard > 150 lines, or an unknown size (fail toward more agents): top-2 domains by score plus the table.
@@ -27,7 +28,7 @@
27
28
  # exact value EXTENDED is STANDARD. An extended roster prints an extra DEPTH=extended line.
28
29
  # Path triggers escalate in EVERY tier except full (which already runs the union): CONTRACT HIGH_RISK, cross-critical
29
30
  # pairs, churn hot-spot, danger zone, bin/engine* (Concurrency + spec-cli), bin/ (spec-cli), auth/session/trust-filter
30
- # files (Auth), CI/docker/deploy/infra files (Infra), migration/.sql files (Database), SDK/OpenAPI files (API).
31
+ # files (Auth: auth*, not author*; oauth, jwt, login, password, permission, rbac, acl, token(s), api_key(s), credential, deps.py), CI/docker/deploy/infra files (Infra), migration/.sql files (Database), SDK/OpenAPI files (API).
31
32
  # Auth and Infra are path-gated: a diff-text score alone never selects them. Security is a hard floor in every tier.
32
33
  # spec-cli is added only with --spec-cli true (the persona file exists in the repo under review); in the standard tier it
33
34
  # also covers commands/**. Bash 3.2 portable (no mapfile, associative arrays, grep -P).
@@ -114,7 +115,10 @@ fi
114
115
  HIGH_RISK=false
115
116
  case " $CFLAGS " in *" HIGH_RISK "*) HIGH_RISK=true ;; esac
116
117
 
117
- if [ "$FIX_PR" = "true" ]; then
118
+ # The fix-pr tier is author-controlled (Closes #N), so it is capped by size: a larger or unknown-size fix PR follows the size ladder.
119
+ FIX_PR_MAX_LINES=150
120
+ FIX_PR_CAPPED=false
121
+ if [ "$FIX_PR" = "true" ] && [ "$LINES_KNOWN" = "true" ] && [ "$LINES" -le "$FIX_PR_MAX_LINES" ]; then
118
122
  TIER=fix-pr; REASON="fix PR for a review-finding issue"
119
123
  elif [ "$LINES_KNOWN" = "true" ] && { [ "$LINES" -le 60 ] || { [ "$NFILES" -eq 1 ] && [ "$LINES" -le 150 ]; }; }; then
120
124
  TIER=small; REASON="lines=$LINES files=$NFILES"
@@ -124,6 +128,9 @@ else
124
128
  TIER=standard
125
129
  if [ "$LINES_KNOWN" = "true" ]; then REASON="lines=$LINES"; else REASON="size unknown"; fi
126
130
  fi
131
+ if [ "$FIX_PR" = "true" ] && [ "$TIER" != "fix-pr" ]; then
132
+ FIX_PR_CAPPED=true; REASON="$REASON; fix-pr tier capped by size (max $FIX_PR_MAX_LINES lines, known size)"
133
+ fi
127
134
  # CONTRACT HIGH_RISK is never narrowed: a small, medium or fix-PR roster is lifted to the standard roster.
128
135
  BASE_TIER="$TIER"
129
136
  if [ "$HIGH_RISK" = "true" ] && [ "$TIER" != "standard" ]; then TIER=standard; REASON="$REASON; CONTRACT HIGH_RISK lifts $BASE_TIER to standard"; fi
@@ -147,6 +154,12 @@ case "$TIER" in
147
154
  esac ;;
148
155
  standard) for d in $(top_domains "$TOP_N"); do add_agent "$d"; done ;;
149
156
  esac
157
+ # A size-capped fix PR keeps the source finding's domain (size only ever ADDS depth).
158
+ if [ "$FIX_PR_CAPPED" = "true" ] && [ -n "$SRC_DOM" ]; then
159
+ case "$(printf '%s' "$SRC_SEV" | tr '[:lower:]' '[:upper:]')" in
160
+ HIGH|CRITICAL|BLOCKER) add_agent "$SRC_DOM"; REASON="$REASON; source finding ${SRC_SEV} adds its domain" ;;
161
+ esac
162
+ fi
150
163
 
151
164
  # Score escalation table (medium and standard only; diff-text signals, so never in the small or fix-pr tiers)
152
165
  if [ "$TIER" = "medium" ] || [ "$TIER" = "standard" ]; then
@@ -170,8 +183,8 @@ if [ "$CHURN" = "true" ] || [ "$DANGER" = "true" ]; then
170
183
  for d in $(top_domains 1); do add_agent "$d"; trig churn-or-danger-zone; done
171
184
  fi
172
185
  hit() { printf '%s\n' "$FILES" | grep -qiE "$1"; }
173
- hit '(^|[/_.-])(auth|authn|authz|session|sessions|jwt|oauth|login|logout|password|trust[-_]?filter|trusted[-_a-z]*)([/_.-]|$)' && { add_agent Auth; trig auth-path; }
174
- hit '^\.github/|(^|/)Dockerfile|docker-compose|(^|/)(deploy|infra|traefik|nginx|k8s|helm|terraform)/|\.tf$' && { add_agent Infra; trig infra-path; }
186
+ hit '(^|[/_.-])(auth([^o]|o[^r]|$)|authori[sz]|oauth|session|jwt|login|logout|passw(or)?d|permission|rbac|acl([/_.-]|$)|tokens?([/_.-]|$)|api[-_]?keys?|credential|trust[-_]?filter|trusted[-_a-z]*)|(^|/)deps\.py$' && { add_agent Auth; trig auth-path; }
187
+ hit '^\.github/|(^|/)Dockerfile|docker-compose|(^|/)(docker-)?compose[^/]*\.ya?ml$|(^|/)deploy[^/]*\.sh$|(^|/)docker-entrypoint[^/]*$|(^|/)(deploy|infra|traefik|nginx|k8s|helm|terraform)/|\.tf$' && { add_agent Infra; trig infra-path; }
175
188
  hit 'migration|\.sql$' && { add_agent Database; trig migration-path; }
176
189
  hit '^sdk/|openapi.*\.json$|openapi-versions/' && { add_agent API; trig sdk-path; }
177
190
  hit '^bin/engine' && { add_agent Concurrency; trig bin-engine; }
@@ -58,9 +58,26 @@ expect_has "consumer repo bin/engine" concurrency; expect_not "consumer repo bin
58
58
 
59
59
  # Case 3: a fix PR gives Security only
60
60
  run "commands/orchestrate/phase-4-execution.md
61
- commands/work-on.md" --scores "$SC_NOISY" --changed-lines 200 --changed-files 2 --fix-pr true --source-severity MEDIUM
61
+ commands/work-on.md" --scores "$SC_NOISY" --changed-lines 120 --changed-files 2 --fix-pr true --source-severity MEDIUM
62
62
  expect_sel "fix PR" "security"; expect_tier "fix PR" fix-pr
63
63
 
64
+ # Case 3b (forge#3848): the fix-pr tier is capped at 150 known lines; larger or unknown-size fix PRs follow the size ladder
65
+ SC_MULTI="AUTH=0 BILLING=5 CONCURRENCY=0 DATABASE=4 INFRA=0 FRONTEND=3 API=0"
66
+ run "svc/x.py" --scores "$SC_MULTI" --changed-lines 900 --changed-files 30 --fix-pr true --source-severity MEDIUM
67
+ expect_tier "fix PR 900 lines" standard; expect_has "fix PR 900 lines" billing; expect_has "fix PR 900 lines" database
68
+ run "svc/x.py" --scores "$SC_QUIET" --changed-lines 150 --changed-files 2 --fix-pr true
69
+ expect_tier "fix-PR-at-cap" fix-pr
70
+ run "svc/x.py" --scores "$SC_QUIET" --changed-lines 151 --changed-files 2 --fix-pr true
71
+ expect_tier "fix PR over cap" standard
72
+ run "svc/x.py" --scores "$SC_MULTI" --fix-pr true
73
+ expect_tier "fix-PR-unknown-size" standard
74
+ run "svc/x.py" --scores "$SC_MULTI" --changed-lines abc --fix-pr true
75
+ expect_tier "fix PR non-numeric size" standard
76
+ run "svc/x.py" --scores "$SC_QUIET" --changed-lines 400 --changed-files 5 --fix-pr true --source-severity HIGH --source-domain Concurrency
77
+ expect_has "capped-fix-PR,-HIGH-source" concurrency; expect_tier "capped-fix-PR,-HIGH-source" standard
78
+ run "svc/x.py" --scores "$SC_QUIET" --changed-lines 400 --changed-files 5 --fix-pr true --contract-flags "HIGH_RISK" --review-depth EXTENDED
79
+ expect_tier "capped-fix-PR-with-HIGH_RISK-and-EXTENDED" standard
80
+
64
81
  # Case 4: a fix PR whose source finding was HIGH adds the domain agent
65
82
  run "commands/work-on.md" --scores "AUTH=0 BILLING=0 CONCURRENCY=2 DATABASE=0 INFRA=0 FRONTEND=0 API=0" --changed-lines 40 --fix-pr true --source-severity HIGH --source-domain Concurrency
66
83
  expect_sel "fix PR, HIGH source finding" "security concurrency"
@@ -89,12 +106,51 @@ run "svc/app/routers/session.py" --scores "$SC_QUIET" --changed-lines 20 --chang
89
106
  expect_has "auth path" auth
90
107
  run "scripts/trusted-comments.sh" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
91
108
  expect_has "trust-filter path" auth
109
+ run "svc/authorization.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
110
+ expect_has "auth path authorization.py" auth
111
+ run "svc/authMiddleware.ts" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
112
+ expect_has "auth-path-authMiddleware.ts" auth
113
+ run "svc/oauth2.ts" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
114
+ expect_has "auth path oauth2.ts" auth
115
+ run "svc/permissions.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
116
+ expect_has "auth path permissions.py" auth
117
+ run "svc/rbac.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
118
+ expect_has "auth path rbac.py" auth
119
+ run "svc/deps.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
120
+ expect_has "auth path deps.py" auth
121
+ run "svc/tokens.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
122
+ expect_has "auth path tokens.py" auth
123
+ run "svc/api_keys.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
124
+ expect_has "auth path api_keys.py" auth
125
+ run "svc/auth_utils.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
126
+ expect_has "auth-path-auth_utils.py" auth
127
+ run "AUTHORS.md" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
128
+ expect_not "no-auth-path-AUTHORS.md" auth
129
+ run "docs/author.md" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
130
+ expect_not "no auth path docs/author.md" auth
131
+ run "svc/tokenizer.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
132
+ expect_not "no-auth-path-svc/tokenizer.py" auth
133
+ run "commands/review-pr.md" --scores "AUTH=3 INFRA=0 DATABASE=0 BILLING=0 CONCURRENCY=0" --changed-lines 20 --changed-files 1
134
+ expect_not "auth-from-prose-score-alone" auth
92
135
  run "commands/review-pr.md" --scores "AUTH=3 INFRA=3 DATABASE=0 BILLING=0 CONCURRENCY=0" --changed-lines 120
93
136
  expect_not "auth from prose score alone" auth; expect_not "infra from prose score alone" infra
94
137
  run ".github/workflows/ci.yml" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
95
138
  expect_has "infra path" infra
96
139
  run "docker-compose.prod.yml" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
97
140
  expect_has "docker path" infra
141
+ # Infra path gate covers deploy scripts, container entrypoints and Compose v2 names (basename-anchored)
142
+ for f in scripts/deploy.sh scripts/deploy-prod.sh docker-entrypoint.sh svc/docker-entrypoint.sh compose.yaml compose.yml; do
143
+ run "$f" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
144
+ expect_has "infra path: $f" infra
145
+ done
146
+ for f in scripts/resolve-deploy-gate.sh commands/deploy-pr.md; do
147
+ run "$f" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
148
+ expect_not "no-infra-path-$f" infra
149
+ done
150
+ run "$(printf 'README.md\nscripts/deploy.sh')" --scores "$SC_QUIET" --changed-lines 20 --changed-files 2
151
+ expect_has "infra-path-multi-file-list" infra
152
+ run "Scripts/Deploy.SH" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
153
+ expect_has "infra-path-case-insensitive" infra
98
154
  run "db/migrations/0042_add.sql" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
99
155
  expect_has "migration path" database
100
156
  run "sdk/node/src/index.ts" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
@@ -22,6 +22,11 @@ cd "$REPO_ROOT" || exit 1
22
22
  # Gate scripts emit best-effort telemetry (forge#3834); suites run with it off so stubbed gh calls never buffer into ~/.forge.
23
23
  # scripts/telemetry.test.sh sets its own environment.
24
24
  export FORGE_TELEMETRY=off
25
+ # Per-run TMPDIR (forge#4016): every suite and child process inherits it and the whole dir is removed at exit,
26
+ # so a temp path a suite forgets to clean never outlives the run (tmpfs quotas fail with EDQUOT otherwise).
27
+ RUN_TMP=$(mktemp -d) || { echo "run-shell-tests: cannot create per-run TMPDIR" >&2; exit 1; }
28
+ trap 'rm -rf "$RUN_TMP"' EXIT
29
+ export TMPDIR="$RUN_TMP"
25
30
 
26
31
  TIMEOUT_SECS="${SHELL_TEST_TIMEOUT:-300}"
27
32
  # WIRE:PROVEN — manual: SHELL_TEST_TIMEOUT=abc and SHELL_TEST_TIMEOUT=0 both print the error and exit 2
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ # selfreview-plan.sh — build-time self-review plan: run or skip, and which roster (forge#3971).
3
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+ #
6
+ # Pure decision script: flags in, key=value lines out. No network, no git, no eval; every flag value is data.
7
+ # It calls scripts/review-roster.sh (resolved through forge_resolve, never from the repo under build) on the staged
8
+ # diff's file list so the self-review roster equals the roster /review-pr would pick for the same diff.
9
+ #
10
+ # Usage: selfreview-plan.sh --files-file PATH|- [--changed-lines N] [--changed-files N] [--contract-flags "HIGH_RISK ..."]
11
+ # [--spec-cli true|false] [--review-depth STANDARD|EXTENDED] [--scores "AUTH=n ..."]
12
+ # [--agent-available true|false] [--tree HASH] [--done-trees "HASH HASH ..."]
13
+ # Env: FORGE_SELFREVIEW=off|0|false|disabled operator off switch
14
+ # Output (stdout, always all four lines):
15
+ # SELFREVIEW=run|skip SKIP_REASON=<reason or empty> TIER=<roster tier or empty> SELECTED_AGENTS=<domains>
16
+ # Skip reasons (first match wins): disabled, empty-diff, docs-only, no-agent-tool, already-done, roster-unavailable,
17
+ # small-security-only (small tier, Security-only roster, no path or contract trigger fired).
18
+ # Exit: 0 for every parseable call, including every degrade-to-skip path (self-review never blocks build); 2 on an unknown flag.
19
+ # Bash 3.2 portable (no mapfile, associative arrays, grep -P).
20
+ set -u
21
+
22
+ FILES_FILE=""; LINES=""; NFILES=""; CFLAGS=""; SPEC_CLI=false; DEPTH=STANDARD; SCORES=""
23
+ AGENT_AVAILABLE=true; TREE=""; DONE_TREES=""
24
+
25
+ while [ $# -gt 0 ]; do
26
+ case "$1" in
27
+ --files-file) FILES_FILE="${2:-}"; shift 2 ;;
28
+ --changed-lines) LINES="${2:-}"; shift 2 ;;
29
+ --changed-files) NFILES="${2:-}"; shift 2 ;;
30
+ --contract-flags) CFLAGS="${2:-}"; shift 2 ;;
31
+ --spec-cli) SPEC_CLI="${2:-false}"; shift 2 ;;
32
+ --review-depth) DEPTH="${2:-STANDARD}"; shift 2 ;;
33
+ --scores) SCORES="${2:-}"; shift 2 ;;
34
+ --agent-available) AGENT_AVAILABLE="${2:-true}"; shift 2 ;;
35
+ --tree) TREE="${2:-}"; shift 2 ;;
36
+ --done-trees) DONE_TREES="${2:-}"; shift 2 ;;
37
+ *) echo "selfreview-plan: unknown argument: $1" >&2; exit 2 ;;
38
+ esac
39
+ done
40
+
41
+ emit() { # <run|skip> <skip reason> <tier> <agents>
42
+ printf 'SELFREVIEW=%s\nSKIP_REASON=%s\nTIER=%s\nSELECTED_AGENTS=%s\n' "$1" "$2" "$3" "$4"
43
+ exit 0
44
+ }
45
+
46
+ case "$(printf '%s' "${FORGE_SELFREVIEW:-}" | tr '[:upper:]' '[:lower:]')" in
47
+ off|0|false|disabled|no) emit skip disabled "" "" ;;
48
+ esac
49
+
50
+ FILES=""
51
+ if [ "$FILES_FILE" = "-" ]; then FILES=$(cat)
52
+ elif [ -n "$FILES_FILE" ] && [ -f "$FILES_FILE" ]; then FILES=$(cat "$FILES_FILE"); fi
53
+ FILES=$(printf '%s\n' "$FILES" | grep -v '^[[:space:]]*$' || true)
54
+
55
+ [ -n "$FILES" ] || emit skip empty-diff "" ""
56
+
57
+ # Docs-only: every file is documentation. commands/**, .agents/** and skills/** are executable spec, never docs.
58
+ CODE_FILES=$(printf '%s\n' "$FILES" | grep -vE '^(docs/|README|CHANGELOG|LICENSE|NOTICE)|\.(md|txt|rst)$' || true)
59
+ SPEC_FILES=$(printf '%s\n' "$FILES" | grep -E '^(commands|\.agents|skills)/.*\.md$' || true)
60
+ [ -n "$CODE_FILES" ] || [ -n "$SPEC_FILES" ] || emit skip docs-only "" ""
61
+
62
+ [ "$AGENT_AVAILABLE" = "true" ] || emit skip no-agent-tool "" ""
63
+
64
+ if [ -n "$TREE" ]; then
65
+ for _t in $DONE_TREES; do
66
+ [ "$_t" = "$TREE" ] && emit skip already-done "" ""
67
+ done
68
+ fi
69
+
70
+ type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; _SELF_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd -P)"; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" "${CLAUDE_PLUGIN_ROOT:-}" "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/* "$_SELF_ROOT"; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; } # allowlist:portability — canonical resolver block; readlink -f is guarded (2>/dev/null) and optional
71
+ ROSTER_SCRIPT=$(forge_resolve review-roster.sh)
72
+ [ -n "$ROSTER_SCRIPT" ] && [ -f "$ROSTER_SCRIPT" ] || emit skip roster-unavailable "" ""
73
+
74
+ set -- --files-file - --contract-flags "$CFLAGS" --spec-cli "$SPEC_CLI" --review-depth "$DEPTH"
75
+ [ -n "$LINES" ] && set -- "$@" --changed-lines "$LINES"
76
+ [ -n "$NFILES" ] && set -- "$@" --changed-files "$NFILES"
77
+ [ -n "$SCORES" ] && set -- "$@" --scores "$SCORES"
78
+ OUT=$(printf '%s\n' "$FILES" | bash "$ROSTER_SCRIPT" "$@" 2>/dev/null) || emit skip roster-unavailable "" ""
79
+
80
+ SEL=$(printf '%s\n' "$OUT" | sed -n 's/^SELECTED_AGENTS=//p' | head -n 1)
81
+ TIER=$(printf '%s\n' "$OUT" | sed -n 's/^TIER=//p' | head -n 1)
82
+ [ -n "$SEL" ] && [ -n "$TIER" ] || emit skip roster-unavailable "" ""
83
+
84
+ # Cost guard: the small tier runs only when a path or contract trigger widened the roster past Security.
85
+ if [ "$TIER" = "small" ] && [ "$SEL" = "security" ]; then emit skip small-security-only "$TIER" "$SEL"; fi
86
+ emit run "" "$TIER" "$SEL"
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # Tests for scripts/selfreview-plan.sh (build-time self-review plan, forge#3971). Run: bash scripts/selfreview-plan.test.sh
6
+ # (bash 3.2 compatible)
7
+ set -u
8
+ DIR=$(cd "$(dirname "$0")" && pwd)
9
+ ROOT=$(cd "$DIR/.." && pwd)
10
+ SCRIPT="$DIR/selfreview-plan.sh"
11
+ pass=0; fail=0
12
+ ok() { pass=$((pass+1)); }
13
+ bad() { fail=$((fail+1)); echo "FAIL: $1"; }
14
+ WORK=$(mktemp -d "${TMPDIR:-/tmp}/selfreview-plan.XXXXXX")
15
+ trap 'rm -rf "$WORK"' EXIT
16
+ export FORGEDOCK_HOME="$ROOT" # the resolver must not pick up an ambient install
17
+ unset FORGE_SELFREVIEW
18
+
19
+ PLAN=""; RC=0
20
+ # run <files, newline separated> <flags...>
21
+ run() {
22
+ printf '%s\n' "$1" > "$WORK/files"; shift
23
+ PLAN=$(bash "$SCRIPT" --files-file "$WORK/files" "$@" 2>/dev/null); RC=$?
24
+ }
25
+ field() { printf '%s\n' "$PLAN" | sed -n "s/^$1=//p"; }
26
+ expect() { # name key want
27
+ local got; got=$(field "$2")
28
+ if [ "$got" = "$3" ]; then ok; else bad "$1: $2='$got', want '$3'"; fi
29
+ }
30
+ has_agent() { case " $(field SELECTED_AGENTS) " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
31
+
32
+ # e1: docs-only and empty diffs skip with a recorded reason
33
+ run "docs/GUIDE.md
34
+ README.md" --changed-lines 40
35
+ expect "docs-only skips" SELFREVIEW skip; expect "docs-only reason" SKIP_REASON docs-only
36
+ run "" --changed-lines 0
37
+ expect "empty diff skips" SELFREVIEW skip; expect "empty reason" SKIP_REASON empty-diff
38
+ run "commands/work-on/build.md" --changed-lines 40 --changed-files 1
39
+ expect "spec markdown is not docs" SELFREVIEW skip
40
+ if [ "$(field SKIP_REASON)" = "docs-only" ]; then bad "commands/*.md treated as docs"; else ok; fi
41
+
42
+ # e2: small tier, Security-only roster, no trigger: skipped; the same size with an auth path runs
43
+ run "scripts/foo.sh" --changed-lines 30 --changed-files 1
44
+ expect "small security-only skips" SELFREVIEW skip; expect "small reason" SKIP_REASON small-security-only; expect "small tier" TIER small
45
+ run "src/auth/session.py
46
+ src/util.py" --changed-lines 30 --changed-files 2
47
+ expect "small + auth path runs" SELFREVIEW run; expect "small + auth tier" TIER small
48
+ has_agent auth && ok || bad "auth path did not add auth"
49
+ has_agent security && ok || bad "security floor missing"
50
+ run "scripts/foo.sh
51
+ scripts/bar.sh" --changed-lines 30 --changed-files 2 --contract-flags "HIGH_RISK"
52
+ expect "small + HIGH_RISK runs" SELFREVIEW run; expect "HIGH_RISK lifts tier" TIER standard
53
+
54
+ # standard tier runs
55
+ run "scripts/foo.sh
56
+ scripts/bar.sh" --changed-lines 400 --changed-files 2
57
+ expect "standard runs" SELFREVIEW run; expect "standard tier" TIER standard; expect "standard no reason" SKIP_REASON ""
58
+ has_agent security && ok || bad "standard roster lacks security"
59
+
60
+ # EXTENDED depth lifts a small roster
61
+ run "scripts/foo.sh" --changed-lines 20 --changed-files 1 --review-depth EXTENDED
62
+ expect "extended depth runs" SELFREVIEW run; expect "extended tier" TIER standard
63
+
64
+ # e3: off switch, no Agent tool, unresolvable roster degrade to skip and exit 0
65
+ FORGE_SELFREVIEW=off run "scripts/a.sh
66
+ scripts/b.sh" --changed-lines 400
67
+ [ "$RC" = 0 ] && ok || bad "off exit code $RC"
68
+ expect "off skips" SELFREVIEW skip; expect "off reason" SKIP_REASON disabled
69
+ run "scripts/a.sh
70
+ scripts/b.sh" --changed-lines 400 --agent-available false
71
+ [ "$RC" = 0 ] && ok || bad "no-agent exit code $RC"
72
+ expect "no agent skips" SELFREVIEW skip; expect "no agent reason" SKIP_REASON no-agent-tool
73
+ FORGEDOCK_HOME=/nonexistent-forge-home run "scripts/a.sh
74
+ scripts/b.sh" --changed-lines 400
75
+ [ "$RC" = 0 ] && ok || bad "unresolvable exit code $RC"
76
+ expect "unresolvable roster skips" SELFREVIEW skip; expect "unresolvable reason" SKIP_REASON roster-unavailable
77
+
78
+ # e4: a tree that already has a completed self-review is not dispatched again
79
+ run "scripts/a.sh
80
+ scripts/b.sh" --changed-lines 400 --tree aaa --done-trees "bbb aaa"
81
+ expect "same tree skips" SELFREVIEW skip; expect "same tree reason" SKIP_REASON already-done
82
+ run "scripts/a.sh
83
+ scripts/b.sh" --changed-lines 400 --tree ccc --done-trees "bbb aaa"
84
+ expect "new tree runs" SELFREVIEW run
85
+
86
+ # usage: unknown flag exits 2
87
+ bash "$SCRIPT" --nope >/dev/null 2>&1; [ "$?" = 2 ] && ok || bad "unknown flag should exit 2"
88
+
89
+ echo "selfreview-plan: ${pass} passed, ${fail} failed"
90
+ [ "$fail" -eq 0 ]
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env bash
2
- # spawn-depth-check.sh — will a /work-on dispatching phase (review, remediate) have the Agent tool?
2
+ # spawn-depth-check.sh — will a /work-on dispatching phase (build self-review, review, remediate) have the Agent tool?
3
3
  # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
  #
6
6
  # Claude Code grants the Agent tool to sub-agents down to CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH layers
7
7
  # below the main conversation (default 3 since 2.1.219; 5 for 2.1.172-2.1.216; 1 for 2.1.217-2.1.218).
8
- # A sub-agent at layer L can spawn only when L < depth. The router invokes review/remediate at
8
+ # A sub-agent at layer L can spawn only when L < depth. The router invokes build/review/remediate at
9
9
  # router layer + 1, so they can spawn reviewers only when router layer + 2 <= depth (forge#3398,
10
- # docs/WORK-ON-RUNTIME.md).
10
+ # docs/WORK-ON-RUNTIME.md). build (B5.6 self-review, forge#3971) uses the same arithmetic but degrades to a recorded
11
+ # skip on FAIL instead of stopping the run.
11
12
  #
12
13
  # Usage: spawn-depth-check.sh [--router-layer N] (N = 0 solo, 1 under /orchestrate; default 0)
13
14
  # Env: CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH explicit limit (wins over the version default)
@@ -69,5 +70,5 @@ if [ "$DEPTH" -ge "$REQUIRED" ]; then
69
70
  echo "SPAWN_DEPTH: OK depth=${DEPTH} required=${REQUIRED} (${SOURCE})"
70
71
  exit 0
71
72
  fi
72
- echo "SPAWN_DEPTH: FAIL depth=${DEPTH} required=${REQUIRED} (${SOURCE}) — review/remediate would run without the Agent tool and could not spawn /review-pr reviewers. Set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH>=${REQUIRED}."
73
+ echo "SPAWN_DEPTH: FAIL depth=${DEPTH} required=${REQUIRED} (${SOURCE}) — build/review/remediate would run without the Agent tool and could not spawn self-review or /review-pr reviewers. Set CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH>=${REQUIRED}."
73
74
  exit 1
@@ -43,5 +43,8 @@ t "env depth 09 decimal" 0 "SPAWN_DEPTH: OK" CLAUDE_CODE_MAX_SUBA
43
43
  # Usage errors.
44
44
  t "bad router layer" 2 "spawn-depth-check:" -- --router-layer x
45
45
  t "unknown flag" 2 "usage:" -- --nope
46
+ # forge#3971: build is a dispatching phase too, and the FAIL text names it (exit codes unchanged)
47
+ out=$(env -u CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH -u FORGE_RUNTIME FORGE_CLAUDE_VERSION=2.1.217 bash "$S" 2>&1)
48
+ case "$out" in *build/review/remediate*) PASS=$((PASS+1)) ;; *) FAILN=$((FAILN+1)); echo "FAIL: FAIL text does not name build ($out)" ;; esac
46
49
  echo "spawn-depth-check: ${PASS} passed, ${FAILN} failed"
47
50
  [ "$FAILN" -eq 0 ]
@@ -55,10 +55,20 @@ for fx in "acceptance_gate phase=review verified=3 failed=0 malformed=1 deferred
55
55
  check "fixture $ev event" "$(grep '^{' "$TD/posted" | jq -r '.event')" "$ev"
56
56
  check "fixture $ev key $key" "$(grep '^{' "$TD/posted" | jq -r --arg k "$key" 'has($k)')" "true"
57
57
  done
58
+ reset; FORGE_PR=9 forge_emit finding_disposition severity=MEDIUM scope=introduced disposition=INPR_FIX
59
+ check "finding_disposition carries scope" "$(grep '^{' "$TD/posted" | jq -r '.scope')" "introduced"
58
60
  # review-side events go to the PR, others to the issue
59
61
  reset; FORGE_PR=9 forge_emit review_panel mode=FULL; grep -q 'issues/9/comments' "$TD/calls" && ok || bad "review_panel targets PR"
60
62
  reset; FORGE_PR=9 forge_emit diff_size lines=1; grep -q 'issues/5/comments' "$TD/calls" && ok || bad "diff_size targets issue"
61
63
 
64
+ # build_selfreview (forge#3971) is a build-side event: posted to the issue even when a PR number is set; ids and counts only
65
+ reset; FORGE_PR=9 forge_emit build_selfreview tier=standard agents=3 findings=4 fixed=2 declined=1 skipped=
66
+ grep -q 'issues/5/comments' "$TD/calls" && ok || bad "build_selfreview targets issue"
67
+ REC=$(grep '^{' "$TD/posted")
68
+ check "build_selfreview event" "$(printf '%s' "$REC" | jq -r '.event')" "build_selfreview"
69
+ check "build_selfreview counts typed" "$(printf '%s' "$REC" | jq -r '.agents + .findings + .fixed + .declined')" "10"
70
+ check "build_selfreview skip reason" "$(reset; forge_emit build_selfreview skipped=small-security-only tier=small; grep '^{' "$TD/posted" | jq -r '.skipped')" "small-security-only"
71
+
62
72
  # 2. Sanitising: reserved keys, bad keys, backticks and newlines never reach the record.
63
73
  reset
64
74
  forge_emit x repo=evil 'a b=1' ok='a`b
@@ -278,6 +278,32 @@ fi
278
278
  # ---------------------------------------------------------------------------
279
279
  # Add target label
280
280
  # ---------------------------------------------------------------------------
281
+ # forge#3567: `gh issue edit --add-label` fails when the label is absent from the repo (a
282
+ # consumer repo not re-synced via `forgedock labels setup`), aborting the transition under
283
+ # `set -e`. Best-effort ensure first: `--force` is a create-or-update, a failure (permissions,
284
+ # rate limit, concurrent create) is ignored so the add below surfaces its own error. Only
285
+ # ForgeDock-managed names are created (fixed table mirroring bin/labels.json, kept in sync by
286
+ # bin/tests/label-bootstrap.test.mjs); a custom FORGE_LABEL_MAP name is never created.
287
+ ensure_label() {
288
+ local name="$1" LBL_COLOR="" LBL_DESC=""
289
+ case "$name" in
290
+ "workflow:investigating") LBL_COLOR="1D76DB"; LBL_DESC="Pipeline: investigation phase in progress. Managed by ForgeDock." ;;
291
+ "workflow:ready-to-build") LBL_COLOR="0075CA"; LBL_DESC="Pipeline: investigation complete, ready for build. Managed by ForgeDock." ;;
292
+ "workflow:building") LBL_COLOR="0052CC"; LBL_DESC="Pipeline: implementation in progress. Managed by ForgeDock." ;;
293
+ "workflow:in-review") LBL_COLOR="5319E7"; LBL_DESC="Pipeline: PR created, under review. Managed by ForgeDock." ;;
294
+ "workflow:remediating") LBL_COLOR="C5DEF5"; LBL_DESC="Pipeline: autonomous remediation pending (no human action needed). Managed by ForgeDock." ;;
295
+ "workflow:awaiting-merge") LBL_COLOR="FF8C00"; LBL_DESC="Pipeline: remediated + re-reviewed, awaiting a human merge decision. Managed by ForgeDock." ;;
296
+ "workflow:merged") LBL_COLOR="0E8A16"; LBL_DESC="Pipeline: PR merged, issue closed. Managed by ForgeDock." ;;
297
+ "workflow:decomposed") LBL_COLOR="BFD4F2"; LBL_DESC="Pipeline: decomposed into sub-issues. Managed by ForgeDock." ;;
298
+ "workflow:invalid") LBL_COLOR="CCCCCC"; LBL_DESC="Pipeline: issue closed as invalid after investigation. Managed by ForgeDock." ;;
299
+ "needs-human") LBL_COLOR="E4E669"; LBL_DESC="Pipeline stalled — requires human intervention. Managed by ForgeDock." ;;
300
+ "workflow:engine-error") LBL_COLOR="B60205"; LBL_DESC="Pipeline stalled — engine/tool failure (not a genuine human-judgment block). Managed by ForgeDock." ;;
301
+ *) return 0 ;;
302
+ esac
303
+ gh label create "$name" --color "$LBL_COLOR" --description "$LBL_DESC" --force "${GH_ARGS[@]}" >/dev/null 2>&1 || true
304
+ }
305
+ ensure_label "$EFFECTIVE_LABEL"
306
+
281
307
  echo "Adding $EFFECTIVE_LABEL to issue #$ISSUE_NUMBER..."
282
308
  gh issue edit "$ISSUE_NUMBER" "${GH_ARGS[@]}" --add-label "$EFFECTIVE_LABEL"
283
309
 
@@ -47,7 +47,7 @@ eq "bodies with no match prints nothing" "$(printf '[]' | bash "$TC" bodies x)"
47
47
  SHA=0123456789abcdef0123456789abcdef01234567
48
48
  OTHER=fedcba9876543210fedcba9876543210fedcba98
49
49
  AGENT_RE="^<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${SHA}(\\r?\\n|\$)"
50
- LOOK_RE="^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->)<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${SHA}(\\r?\\n|\$)"
50
+ LOOK_RE="^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:[a-z-]+ -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${SHA}(\\r?\\n|\$)"
51
51
  SYNTH_RE="^<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${SHA}(\\r?\\n|\$)"
52
52
  NL=$'\n'
53
53
  AG_BODY="<!-- FORGE:REVIEW-AGENT:security -->${NL}Reviewed-SHA: ${SHA}${NL}<!-- REVIEW-FINDINGS-START -->${NL}<!-- FINDING:real-1 -->${NL}<!-- REVIEW-FINDINGS-END -->"
@@ -57,6 +57,7 @@ AGJ="[$(c NONE Bot b "$AG_BODY"),$(c NONE User h "$FAKE_AG")]"
57
57
  eq "agent regex counts only the trusted body" "$(printf '%s' "$AGJ" | bash "$TC" count "$AGENT_RE")" 1
58
58
  eq "forged FINDING id never extracted" "$(printf '%s' "$AGJ" | bash "$TC" bodies "$AGENT_RE" | jq -r 'scan("<!-- FINDING:(.+?) -->") | .[0]')" "real-1"
59
59
  eq "lookahead agent count (REVIEW-FINDINGS-START inside body)" "$(printf '%s' "$AGJ" | bash "$TC" count "$LOOK_RE")" 1
60
+ eq "lookahead rejects START without END (truncated)" "$(printf '%s' "[$(c NONE Bot b "$FAKE_AG")]" | bash "$TC" count "$LOOK_RE")" 0
60
61
  eq "lookahead rejects body without REVIEW-FINDINGS-START" "$(printf '%s' "[$(c NONE Bot b "<!-- FORGE:REVIEW-AGENT:api -->${NL}Reviewed-SHA: ${SHA}")]" | bash "$TC" count "$LOOK_RE")" 0
61
62
  eq "agent regex rejects a different head SHA" "$(printf '%s' "[$(c NONE Bot b "<!-- FORGE:REVIEW-AGENT:api -->${NL}Reviewed-SHA: ${OTHER}")]" | bash "$TC" count "$AGENT_RE")" 0
62
63
  eq "agent regex rejects SHA line mid-line" "$(printf '%s' "[$(c NONE Bot b "<!-- FORGE:REVIEW-AGENT:api -->${NL}note Reviewed-SHA: ${SHA}")]" | bash "$TC" count "$AGENT_RE")" 0
@@ -52,11 +52,13 @@ mk() {
52
52
  CONTEXT) items+=('{"body":"<!-- FORGE:CONTEXT -->\n## Context"}') ;;
53
53
  ARCH) items+=('{"body":"<!-- FORGE:ARCHITECT -->\n## Plan"}') ;;
54
54
  QG_PASS) items+=('{"body":"<!-- FORGE:QUALITY_GATE -->\n**Result**: PASS\n"}') ;;
55
+ QG_DEGRADED) items+=('{"body":"<!-- FORGE:QUALITY_GATE -->\n## Quality Gate Result\n\n**Result**: PASS\n**Tree**: abc\n**Degraded**: python.test — test run exercised nothing; blast-radius — check did not complete\n"}') ;;
55
56
  QG_FAIL) items+=('{"body":"<!-- FORGE:QUALITY_GATE -->\n**Result**: FAIL\n"}') ;;
56
57
  FP_NOBAND) items+=('{"body":"<!-- FORGE:FAST_PATH -->\nnothing"}') ;;
57
58
  FP_*) items+=("{\"body\":\"<!-- FORGE:FAST_PATH -->\\n**COMPLEXITY_BAND**: ${k#FP_}\"}") ;;
58
59
  QUOTED) items+=('{"body":"see `<!-- FORGE:CONTRACT -->` `<!-- FORGE:CONTEXT -->` `<!-- FORGE:ARCHITECT -->` `<!-- FORGE:QUALITY_GATE -->` **Result**: PASS"}') ;;
59
60
  INV_INVALID) items+=('{"body":"<!-- FORGE:INVESTIGATOR -->\nInvalid\n<!-- INVESTIGATION:INVALID -->"}') ;;
61
+ SR) items+=('{"body":"<!-- FORGE:SELFREVIEW-AGENT:security -->\nTree: abc\n<!-- REVIEW-FINDINGS-START -->\n<!-- REVIEW-FINDINGS-END -->"}' '{"body":"<!-- FORGE:SELFREVIEW -->\n## Self-review\nTree: abc"}') ;;
60
62
  PROSE) items+=('{"body":"I ran the architect and context inline; FORGE:CONTEXT FORGE:ARCHITECT FORGE:QUALITY_GATE done"}') ;;
61
63
  esac
62
64
  done
@@ -81,6 +83,11 @@ expect_pass "lowercase band is accepted" "$(mk lower INV CONTRACT FP_trivial QG_
81
83
  expect_pass "Investigation band passes with only INVESTIGATOR+FAST_PATH" "$(mk invest INV FP_INVESTIGATION)"
82
84
  expect_pass "docs-only waives QUALITY_GATE" "$(mk docs INV CONTRACT FP_TRIVIAL)" --docs-only
83
85
 
86
+ # forge#3971: build-time self-review comments are tolerated but never satisfy a required phase marker
87
+ expect_pass "self-review comments do not disturb a full trail" "$(mk sr1 INV CONTRACT FP_COMPLEX CONTEXT ARCH QG_PASS SR)"
88
+ expect_fail "self-review comments do not stand in for QUALITY_GATE" "$(mk sr2 INV CONTRACT FP_COMPLEX CONTEXT ARCH SR)" QUALITY_GATE
89
+ expect_fail "self-review comments do not stand in for CONTEXT" "$(mk sr3 INV CONTRACT FP_STANDARD ARCH QG_PASS SR)" CONTEXT
90
+
84
91
  expect_fail "missing INVESTIGATOR" "$(mk a CONTRACT FP_COMPLEX CONTEXT ARCH QG_PASS)" INVESTIGATOR
85
92
  expect_fail "partial investigation (no COMPLETE) fails" "$(mk b INV_PARTIAL CONTRACT FP_COMPLEX CONTEXT ARCH QG_PASS)" INVESTIGATOR
86
93
  expect_fail "missing CONTRACT" "$(mk c INV FP_COMPLEX CONTEXT ARCH QG_PASS)" CONTRACT
@@ -90,6 +97,7 @@ expect_fail "missing FAST_PATH" "$(mk f INV CONTRACT CONTEXT ARCH QG_PASS)" FAST
90
97
  expect_fail "FAST_PATH without band value fails" "$(mk g INV CONTRACT FP_NOBAND CONTEXT ARCH QG_PASS)" FAST_PATH
91
98
  expect_fail "missing QUALITY_GATE" "$(mk h INV CONTRACT FP_COMPLEX CONTEXT ARCH)" QUALITY_GATE
92
99
  expect_fail "QUALITY_GATE FAIL result does not count" "$(mk i INV CONTRACT FP_COMPLEX CONTEXT ARCH QG_FAIL)" QUALITY_GATE
100
+ expect_pass "QUALITY_GATE PASS with a **Degraded**: line still counts (forge#3628)" "$(mk i2 INV CONTRACT FP_COMPLEX CONTEXT ARCH QG_DEGRADED)"
93
101
  expect_fail "TRIVIAL still needs CONTRACT" "$(mk j INV FP_TRIVIAL QG_PASS)" CONTRACT
94
102
  expect_fail "prose mentions do not satisfy markers" "$(mk k INV CONTRACT FP_COMPLEX PROSE)" CONTEXT
95
103
 
@@ -263,6 +271,12 @@ expect_pass "head-tree matches the PASS marker's Tree" "$(ht h1 "$QG_T1")" --hea
263
271
  expect_fail "PASS recorded for an earlier tree does not satisfy the gate" "$(ht h2 "$QG_T2")" QUALITY_GATE --head-tree "$T1"
264
272
  expect_fail "PASS without a Tree line does not satisfy a bound gate" "$(ht h3 "$QG_NOTREE")" QUALITY_GATE --head-tree "$T1"
265
273
  expect_pass "a later PASS for the head tree wins over an earlier-tree PASS" "$(ht h4 "$QG_T2" "$QG_T1")" --head-tree "$T1"
274
+ # Post-merge re-gate (#3983): T1 = pre-sync build tree, T2 = merged tree after the base sync
275
+ expect_fail "stale pre-merge PASS does not satisfy the merged head" "$(ht m1 "$QG_T1")" QUALITY_GATE --head-tree "$T2"
276
+ expect_pass "post-merge re-gate marker satisfies the merged head" "$(ht m2 "$QG_T1" "$QG_T2")" --head-tree "$T2"
277
+ expect_pass "pre-merge PASS still satisfies the pre-merge tree" "$(ht m3 "$QG_T1" "$QG_T2")" --head-tree "$T1"
278
+ QG_T2_FAIL=$'<!-- FORGE:QUALITY_GATE -->\n**Result**: FAIL\n**Tree**: '"$T2"
279
+ expect_fail "a FAIL re-gate on the merged tree is not a PASS" "$(ht m4 "$QG_T2_FAIL")" QUALITY_GATE --head-tree "$T2"
266
280
  expect_pass "no --head-tree keeps the unbound behaviour" "$(ht h5 "$QG_NOTREE")"
267
281
  expect_pass "uppercase head-tree is normalised" "$(ht h6 "$QG_T1")" --head-tree "$(echo "$T1" | tr a-f A-F)"
268
282
  expect_pass "docs-only still waives the bound gate" "$(ht h7)" --docs-only --head-tree "$T1"