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.
- package/README.md +2 -0
- 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 +187 -10
- package/bin/runner.mjs +89 -13
- 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/orchestrate/phase-4-execution.md +1 -1
- package/commands/review-pr-agents/protocols.md +19 -0
- package/commands/review-pr-staging.md +4 -4
- package/commands/review-pr.md +31 -20
- package/commands/work-on/build/architect.md +54 -1
- package/commands/work-on/build/implement.md +28 -1
- package/commands/work-on/build/validate.md +321 -29
- package/commands/work-on/build.md +184 -17
- package/commands/work-on/close.md +9 -4
- package/commands/work-on/investigate.md +7 -0
- package/commands/work-on/remediate.md +43 -17
- package/commands/work-on/review.md +43 -22
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +93 -2
- package/docs/OUTCOME-CODES.md +5 -1
- package/docs/RELEASE-NOTES.md +15 -0
- package/docs/STATS.md +11 -4
- package/docs/TELEMETRY.md +5 -2
- package/docs/WORK-ON-RUNTIME.md +29 -3
- 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/architect-resume-match.test.sh +12 -3
- package/scripts/build-knowledge-index.mjs +8 -5
- package/scripts/builder-resume-match.test.sh +12 -3
- package/scripts/calibration.mjs +5 -2
- 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 +4 -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 +379 -0
- package/scripts/ci-local/plan.mjs +186 -0
- package/scripts/ci-local/run.mjs +451 -0
- package/scripts/ci-local/scoping.mjs +531 -0
- package/scripts/ci-local.sh +98 -0
- package/scripts/ci-local.test.sh +1108 -0
- package/scripts/classify-finding.sh +33 -7
- 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/file-deferred-followups.sh +91 -30
- package/scripts/file-deferred-followups.test.sh +101 -8
- 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 +3 -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 +294 -53
- package/scripts/review-finalize.test.sh +347 -9
- 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/review-roster.sh +18 -5
- package/scripts/review-roster.test.sh +57 -1
- package/scripts/run-shell-tests.sh +5 -0
- package/scripts/selfreview-plan.sh +86 -0
- package/scripts/selfreview-plan.test.sh +90 -0
- package/scripts/spawn-depth-check.sh +5 -4
- package/scripts/spawn-depth-check.test.sh +3 -0
- package/scripts/telemetry.test.sh +10 -0
- package/scripts/transition-label.sh +26 -0
- package/scripts/trusted-comments.test.sh +2 -1
- package/scripts/verify-phase-trail.test.sh +14 -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
|
@@ -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
|
-
|
|
129
|
-
|
|
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
|
|
package/scripts/review-roster.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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|
|
|
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
|
|
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"
|