forgedock 1.16.2 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/outcome-codes.mjs +114 -0
- package/bin/engine/phases.mjs +211 -100
- package/bin/engine/projector.mjs +48 -1
- package/bin/engine/runlog.mjs +20 -5
- package/bin/engine/telemetry-seam.mjs +62 -0
- package/bin/engine/telemetry-seam.test.mjs +60 -0
- package/bin/engine/telemetry.mjs +148 -0
- package/bin/engine/telemetry.test.mjs +120 -0
- package/bin/engine-cli.mjs +17 -1
- package/bin/engine.mjs +227 -48
- package/bin/forgedock.mjs +8 -2
- package/bin/hooks/interactive-engine.mjs +10 -0
- package/bin/hooks/pre-tool-use.mjs +220 -0
- package/bin/runner.mjs +70 -4
- package/bin/stats/aggregate.mjs +231 -0
- package/bin/stats/cache.mjs +40 -0
- package/bin/stats/github-source.mjs +211 -0
- package/bin/stats/index.mjs +154 -0
- package/bin/stats/local-source.mjs +79 -0
- package/bin/stats/package.json +6 -0
- package/bin/stats/render.mjs +82 -0
- package/bin/stats/stats.test.mjs +312 -0
- package/commands/cleanup.md +90 -2
- package/commands/deploy-info.md +8 -4
- package/commands/deploy-pr.md +64 -0
- package/commands/fix-ci.md +20 -6
- package/commands/milestone.md +34 -17
- package/commands/orchestrate/phase-1-resolve.md +4 -1
- package/commands/orchestrate/phase-4-execution.md +28 -6
- package/commands/orchestrate/phase-5-cleanup.md +18 -2
- package/commands/pipeline-health/phase-1-context.md +2 -0
- package/commands/pipeline-health/phase-2-metrics.md +12 -5
- package/commands/pipeline-status.md +21 -1
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents.md +10 -2
- package/commands/review-pr-staging.md +196 -496
- package/commands/review-pr.md +512 -3134
- package/commands/test-gate.md +25 -10
- package/commands/work-on/build/architect.md +13 -1
- package/commands/work-on/build/implement.md +28 -6
- package/commands/work-on/build/validate.md +5 -2
- package/commands/work-on/build.md +197 -145
- package/commands/work-on/close.md +66 -20
- package/commands/work-on/decompose.md +113 -34
- package/commands/work-on/investigate.md +15 -14
- package/commands/work-on/remediate.md +49 -19
- package/commands/work-on/review.md +7 -5
- package/commands/work-on.md +1 -2
- package/docs/CONFIG.md +76 -24
- package/docs/OUTCOME-CODES.md +53 -0
- package/docs/RELEASE-NOTES.md +24 -0
- package/docs/STATS.md +63 -0
- package/docs/TELEMETRY.md +65 -0
- package/docs/WORK-ON-RUNTIME.md +19 -0
- package/docs/site/command-reference.md +1 -1
- package/docs/spec/forge-protocol-v1.md +2 -2
- package/docs/spec/label-state-machine.md +1 -1
- package/package.json +3 -2
- package/packages/protocol/src/phases.js +5 -5
- package/scripts/builder-resume-match.test.sh +58 -0
- package/scripts/cap-findings.sh +110 -0
- package/scripts/cap-findings.test.sh +130 -0
- package/scripts/check-spec-bash.sh +5 -1
- package/scripts/check-spec-bash.test.sh +20 -0
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/classify-finding.sh +65 -16
- package/scripts/classify-finding.test.sh +43 -1
- package/scripts/classify-grep-exit.sh +20 -0
- package/scripts/classify-grep-exit.test.sh +46 -0
- package/scripts/close-parent-c3.test.sh +123 -0
- package/scripts/cost-report.sh +146 -0
- package/scripts/cost-report.test.sh +80 -0
- package/scripts/diff-size.sh +97 -3
- package/scripts/diff-size.test.sh +59 -2
- package/scripts/doctor-pipeline-state.sh +43 -0
- package/scripts/file-deferred-followups.sh +159 -0
- package/scripts/file-deferred-followups.test.sh +106 -0
- package/scripts/fixtures/cost-report/a-merged1.jsonl +6 -0
- package/scripts/fixtures/cost-report/a-merged2-remediated.jsonl +7 -0
- package/scripts/fixtures/cost-report/a-needs-human.jsonl +4 -0
- package/scripts/fixtures/cost-report/b-merged.jsonl +3 -0
- package/scripts/fixtures/cost-report/c-merged-attempt-usage.jsonl +8 -0
- package/scripts/fixtures/cost-report/hydrated-no-usage.jsonl +3 -0
- package/scripts/fixtures/cost-report/old-log-no-version.jsonl +3 -0
- package/scripts/fixtures/cost-report/unfinished-no-terminal.jsonl +2 -0
- package/scripts/fixtures/cost-report/unfinished-reopened.jsonl +5 -0
- package/scripts/forge-root.test.sh +18 -11
- package/scripts/lib/fence-bootstrap.sh +4 -3
- package/scripts/lib/pr-issue-number.sh +18 -0
- package/scripts/lib/review-template-resolve.sh +41 -0
- package/scripts/lib/telemetry.sh +168 -0
- package/scripts/publish-staging-sync.test.sh +48 -0
- package/scripts/remediate-liveness.sh +42 -6
- package/scripts/resolve-deploy-gate.sh +32 -6
- package/scripts/resolve-deploy-gate.test.sh +42 -1
- package/scripts/review-acceptance-gate.sh +16 -7
- package/scripts/review-acceptance-gate.test.sh +13 -0
- package/scripts/review-finalize.sh +1427 -0
- package/scripts/review-finalize.test.sh +833 -0
- package/scripts/review-pr-delta-wiring.test.sh +19 -72
- package/scripts/review-pr-domain-map.test.sh +23 -13
- package/scripts/review-pr-gate2.test.sh +24 -65
- package/scripts/review-pr-staging-finding-filter.test.sh +44 -0
- package/scripts/review-pr-staging-scope.test.sh +56 -80
- package/scripts/review-prepare.sh +553 -0
- package/scripts/review-prepare.test.sh +288 -0
- package/scripts/review-roster.sh +186 -0
- package/scripts/review-roster.test.sh +185 -0
- package/scripts/run-shell-tests.sh +3 -0
- package/scripts/telemetry.test.sh +106 -0
- package/scripts/trusted-comments.test.sh +9 -2
- package/scripts/wait-ci-green.sh +8 -1
- package/scripts/wait-ci-green.test.sh +3 -3
- package/templates/forge.yaml.minimal +9 -0
|
@@ -0,0 +1,185 @@
|
|
|
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/review-roster.sh (risk-tiered reviewer roster, forge#3599): the tier cases, the path triggers that
|
|
6
|
+
# escalate in every tier, and the regression table of the 13 review-driven holds. Run: bash scripts/review-roster.test.sh
|
|
7
|
+
# (bash 3.2 compatible)
|
|
8
|
+
#
|
|
9
|
+
# Hold table fixture: the changed-line count, file count and file list of each PR, the PR's linked-issue lineage, and
|
|
10
|
+
# the reviewers whose posted findings were CRITICAL or BLOCKER on it (from the FORGE:REVIEW-AGENT comments). The
|
|
11
|
+
# regression gate: at least one catching reviewer stays selected on every hold, and the sole catcher always does.
|
|
12
|
+
|
|
13
|
+
set -u
|
|
14
|
+
DIR=$(cd "$(dirname "$0")" && pwd)
|
|
15
|
+
SCRIPT="$DIR/review-roster.sh"
|
|
16
|
+
pass=0; fail=0
|
|
17
|
+
ok() { pass=$((pass+1)); }
|
|
18
|
+
bad() { fail=$((fail+1)); echo "FAIL: $1"; }
|
|
19
|
+
WORK=$(mktemp -d "${TMPDIR:-/tmp}/review-roster.XXXXXX")
|
|
20
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
21
|
+
|
|
22
|
+
SEL=""; TIER=""; REASON=""
|
|
23
|
+
# run <files, newline separated> <review-roster flags...>
|
|
24
|
+
run() {
|
|
25
|
+
printf '%s\n' "$1" > "$WORK/files"; shift
|
|
26
|
+
out=$(bash "$SCRIPT" --files-file "$WORK/files" "$@") || { bad "script exited non-zero for: $*"; return 1; }
|
|
27
|
+
SEL=$(printf '%s\n' "$out" | sed -n 's/^SELECTED_AGENTS=//p')
|
|
28
|
+
TIER=$(printf '%s\n' "$out" | sed -n 's/^TIER=//p')
|
|
29
|
+
REASON=$(printf '%s\n' "$out" | sed -n 's/^REASON=//p')
|
|
30
|
+
}
|
|
31
|
+
has() { case " $SEL " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
|
32
|
+
expect_sel() { if [ "$SEL" = "$2" ]; then ok; else bad "$1: roster '$SEL', want '$2' (tier $TIER; $REASON)"; fi; }
|
|
33
|
+
expect_has() { if has "$2"; then ok; else bad "$1: '$2' missing from '$SEL' (tier $TIER; $REASON)"; fi; }
|
|
34
|
+
expect_not() { if has "$2"; then bad "$1: '$2' should not be in '$SEL' (tier $TIER)"; else ok; fi; }
|
|
35
|
+
expect_tier() { if [ "$TIER" = "$2" ]; then ok; else bad "$1: tier '$TIER', want '$2'"; fi; }
|
|
36
|
+
|
|
37
|
+
SC_QUIET="AUTH=0 BILLING=0 CONCURRENCY=0 DATABASE=0 INFRA=0 SECURITY=1 SCRAPING=0 FRONTEND=0 API=0"
|
|
38
|
+
SC_NOISY="AUTH=3 BILLING=1 CONCURRENCY=0 DATABASE=1 INFRA=3 SECURITY=4 SCRAPING=0 FRONTEND=2 API=2"
|
|
39
|
+
SC_BUSY="AUTH=3 BILLING=3 CONCURRENCY=3 DATABASE=3 INFRA=3 SECURITY=1 SCRAPING=0 FRONTEND=2 API=2"
|
|
40
|
+
|
|
41
|
+
# Case 1: a small diff gives Security only, even when diff-text scores are loud
|
|
42
|
+
run "commands/foo.md" --scores "$SC_NOISY" --changed-lines 40 --changed-files 1
|
|
43
|
+
expect_sel "small diff" "security"; expect_tier "small diff" small
|
|
44
|
+
run "scripts/a.sh
|
|
45
|
+
scripts/b.sh" --scores "$SC_NOISY" --changed-lines 60 --changed-files 2
|
|
46
|
+
expect_sel "60-line boundary" "security"
|
|
47
|
+
# a single file is small up to 150 lines, not beyond
|
|
48
|
+
run "scripts/a.sh" --scores "$SC_QUIET" --changed-lines 120 --changed-files 1
|
|
49
|
+
expect_tier "single file" small
|
|
50
|
+
run "scripts/a.sh" --scores "$SC_QUIET" --changed-lines 400 --changed-files 1
|
|
51
|
+
expect_tier "single large file" standard
|
|
52
|
+
|
|
53
|
+
# Case 2: a small diff touching bin/engine/** adds concurrency and spec-cli
|
|
54
|
+
run "bin/engine/phases.mjs" --scores "$SC_QUIET" --changed-lines 30 --changed-files 1 --spec-cli true
|
|
55
|
+
expect_has "small bin/engine" concurrency; expect_has "small bin/engine" spec-cli; expect_has "small bin/engine" security; expect_tier "small bin/engine" small
|
|
56
|
+
run "bin/engine/phases.mjs" --scores "$SC_QUIET" --changed-lines 30 --changed-files 1 --spec-cli false
|
|
57
|
+
expect_has "consumer repo bin/engine" concurrency; expect_not "consumer repo bin/engine" spec-cli
|
|
58
|
+
|
|
59
|
+
# Case 3: a fix PR gives Security only
|
|
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
|
|
62
|
+
expect_sel "fix PR" "security"; expect_tier "fix PR" fix-pr
|
|
63
|
+
|
|
64
|
+
# Case 4: a fix PR whose source finding was HIGH adds the domain agent
|
|
65
|
+
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
|
+
expect_sel "fix PR, HIGH source finding" "security concurrency"
|
|
67
|
+
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 critical
|
|
68
|
+
expect_sel "fix PR, CRITICAL source, top domain" "security concurrency"
|
|
69
|
+
|
|
70
|
+
# Case 5: CONTRACT HIGH_RISK escalates in every tier
|
|
71
|
+
for tier_args in "--changed-lines 30 --changed-files 1" "--changed-lines 100" "--changed-lines 400" "--changed-lines 30 --fix-pr true"; do
|
|
72
|
+
# shellcheck disable=SC2086
|
|
73
|
+
run "svc/x.py" --scores "$SC_QUIET" $tier_args --contract-flags "HIGH_RISK CONTRACT_AUTH CONTRACT_BILLING"
|
|
74
|
+
expect_has "HIGH_RISK ($tier_args)" auth; expect_has "HIGH_RISK ($tier_args)" billing; expect_has "HIGH_RISK ($tier_args)" concurrency
|
|
75
|
+
done
|
|
76
|
+
run "svc/x.py" --scores "$SC_QUIET" --changed-lines 30 --contract-flags "CONTRACT_AUTH"
|
|
77
|
+
expect_not "CONTRACT_AUTH without HIGH_RISK" auth
|
|
78
|
+
|
|
79
|
+
# Case 6: --thorough and milestone give the full union
|
|
80
|
+
run "svc/x.py" --scores "$SC_BUSY" --changed-lines 10 --thorough true
|
|
81
|
+
expect_tier "thorough" full
|
|
82
|
+
for d in security auth billing concurrency database infra frontend api; do expect_has "thorough" "$d"; done
|
|
83
|
+
expect_not "thorough, scraping not configured" scraper
|
|
84
|
+
run "svc/x.py" --scores "$SC_BUSY SCRAPING=3" --changed-lines 10 --milestone true --scraping-configured true
|
|
85
|
+
expect_tier "milestone" full; expect_has "milestone" scraper
|
|
86
|
+
|
|
87
|
+
# Path gates: auth and infra only by path; the other path triggers fire in the small tier
|
|
88
|
+
run "svc/app/routers/session.py" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
89
|
+
expect_has "auth path" auth
|
|
90
|
+
run "scripts/trusted-comments.sh" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
91
|
+
expect_has "trust-filter path" auth
|
|
92
|
+
run "commands/review-pr.md" --scores "AUTH=3 INFRA=3 DATABASE=0 BILLING=0 CONCURRENCY=0" --changed-lines 120
|
|
93
|
+
expect_not "auth from prose score alone" auth; expect_not "infra from prose score alone" infra
|
|
94
|
+
run ".github/workflows/ci.yml" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
95
|
+
expect_has "infra path" infra
|
|
96
|
+
run "docker-compose.prod.yml" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
97
|
+
expect_has "docker path" infra
|
|
98
|
+
run "db/migrations/0042_add.sql" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
99
|
+
expect_has "migration path" database
|
|
100
|
+
run "sdk/node/src/index.ts" --scores "$SC_QUIET" --changed-lines 20 --changed-files 1
|
|
101
|
+
expect_has "sdk path" api
|
|
102
|
+
run "svc/x.py" --scores "AUTH=2 BILLING=2" --changed-lines 20 --changed-files 1
|
|
103
|
+
expect_has "cross-critical" auth; expect_has "cross-critical" billing
|
|
104
|
+
run "svc/x.py" --scores "BILLING=3 CONCURRENCY=3" --changed-lines 20 --changed-files 1 --churn true
|
|
105
|
+
expect_has "churn hot-spot top domain" billing
|
|
106
|
+
run "svc/x.py" --scores "BILLING=3" --changed-lines 20 --changed-files 2
|
|
107
|
+
expect_not "billing score in small tier" billing
|
|
108
|
+
run "svc/x.py
|
|
109
|
+
svc/y.py" --scores "BILLING=3" --changed-lines 100
|
|
110
|
+
expect_has "billing score in medium tier" billing; expect_has "billing score in medium tier" concurrency
|
|
111
|
+
|
|
112
|
+
# Tier shape: standard keeps the top-2 baseline, medium does not
|
|
113
|
+
run "web/src/app/a.tsx
|
|
114
|
+
web/src/b.tsx" --scores "FRONTEND=3 API=2 BILLING=1 DATABASE=1" --changed-lines 300
|
|
115
|
+
expect_tier "standard" standard; expect_has "standard" frontend; expect_has "standard" api; expect_not "standard top-2 only" billing
|
|
116
|
+
run "web/src/app/a.tsx
|
|
117
|
+
web/src/b.tsx" --scores "FRONTEND=3 API=2" --changed-lines 100
|
|
118
|
+
expect_sel "medium has no baseline" "security"
|
|
119
|
+
run "svc/x.py" --scores "FRONTEND=3" --changed-lines ""
|
|
120
|
+
expect_tier "unknown size" standard; expect_has "unknown size" frontend
|
|
121
|
+
run "commands/x.md
|
|
122
|
+
commands/y.md" --scores "$SC_QUIET" --changed-lines 300 --spec-cli true
|
|
123
|
+
expect_has "standard commands/** adds spec-cli" spec-cli
|
|
124
|
+
run "commands/x.md
|
|
125
|
+
commands/y.md" --scores "$SC_QUIET" --changed-lines 100 --spec-cli true
|
|
126
|
+
expect_not "medium commands/** has no spec-cli" spec-cli
|
|
127
|
+
|
|
128
|
+
# Roster output is canonical and duplicate free
|
|
129
|
+
run "bin/engine/a.mjs
|
|
130
|
+
.github/workflows/a.yml" --scores "$SC_BUSY" --changed-lines 300 --spec-cli true
|
|
131
|
+
n=$(printf '%s' "$SEL" | tr ' ' '\n' | grep -c .); u=$(printf '%s' "$SEL" | tr ' ' '\n' | sort -u | grep -c .)
|
|
132
|
+
if [ "$n" -eq "$u" ]; then ok; else bad "duplicate roster entries in '$SEL'"; fi
|
|
133
|
+
case "$SEL" in *[A-Z]*) bad "non-canonical roster '$SEL'" ;; *) ok ;; esac
|
|
134
|
+
|
|
135
|
+
# Unknown flag fails (callers fall back to the full union)
|
|
136
|
+
if bash "$SCRIPT" --bogus 1 >/dev/null 2>&1; then bad "unknown flag accepted"; else ok; fi
|
|
137
|
+
|
|
138
|
+
# Regression table: the 13 review-driven holds. Columns: PR | lines | files | fix-PR | catchers (CRITICAL/BLOCKER) | sole catcher | file list
|
|
139
|
+
hold() {
|
|
140
|
+
pr="$1"; lines="$2"; nf="$3"; fixpr="$4"; catchers="$5"; sole="$6"; files="$7"
|
|
141
|
+
run "$(printf '%s' "$files" | tr ' ' '\n')" --scores "$SC_QUIET" --changed-lines "$lines" --changed-files "$nf" --fix-pr "$fixpr" --spec-cli true
|
|
142
|
+
found=""
|
|
143
|
+
for c in $catchers; do has "$c" && found=1; done
|
|
144
|
+
if [ -n "$found" ]; then ok; else bad "hold #$pr: none of [$catchers] selected: '$SEL' (tier $TIER)"; fi
|
|
145
|
+
if [ -n "$sole" ]; then if has "$sole"; then ok; else bad "hold #$pr: sole catcher '$sole' not selected: '$SEL'"; fi; fi
|
|
146
|
+
if has security; then ok; else bad "hold #$pr: security missing"; fi
|
|
147
|
+
}
|
|
148
|
+
hold 3426 126 3 true "security concurrency" "" "commands/orchestrate/phase-4-execution.md commands/work-on.md commands/work-on/remediate.md"
|
|
149
|
+
hold 3438 227 8 false "spec-cli" "spec-cli" "commands/review-pr-agents/protocols.md commands/review-pr.md commands/work-on.md commands/work-on/remediate.md commands/work-on/review.md docs/WORK-ON-RUNTIME.md docs/spec/review-protocol.md scripts/forge-root.test.sh"
|
|
150
|
+
hold 3461 454 12 false "security spec-cli" "" "commands/work-on.md commands/work-on/build.md commands/work-on/build/implement.md commands/work-on/close.md commands/work-on/decompose.md docs/CONFIG.md docs/spec/forge-protocol-v1.md forge.yaml.example packages/protocol/src/types.js scripts/check-spec-markers.sh scripts/diff-size.sh scripts/diff-size.test.sh"
|
|
151
|
+
hold 3464 53 2 true "security concurrency" "" "commands/orchestrate/phase-4-execution.md commands/work-on.md"
|
|
152
|
+
hold 3480 56 3 true "security" "" "commands/orchestrate/phase-4-execution.md commands/work-on.md scripts/check-spec-markers.sh"
|
|
153
|
+
hold 3490 220 3 false "security" "" "commands/review-pr-staging.md commands/review-pr.md scripts/review-pr-arguments.test.sh"
|
|
154
|
+
hold 3500 263 5 false "security" "" ".github/workflows/check-commands.yml commands/work-on/build/validate.md commands/work-on/review.md scripts/check-branch-ancestry.sh scripts/check-branch-ancestry.test.sh"
|
|
155
|
+
hold 3502 813 11 false "concurrency" "concurrency" "bin/engine-cli.mjs bin/engine.mjs bin/engine/phases-args.test.mjs bin/engine/phases.mjs bin/engine/runlog.mjs bin/tests/engine-crash.test.mjs bin/tests/engine-phases.test.mjs bin/tests/engine-runlog.test.mjs bin/tests/engine.test.mjs docs/WORK-ON-RUNTIME.md packages/protocol/src/phases.js"
|
|
156
|
+
hold 3509 274 6 false "security spec-cli" "" "bin/engine.mjs bin/engine/phases.mjs bin/tests/engine-crash.test.mjs bin/tests/engine-phases.test.mjs bin/tests/engine-runlog.test.mjs bin/tests/engine.test.mjs"
|
|
157
|
+
hold 3513 205 7 false "security concurrency infra spec-cli" "" "commands/orchestrate/phase-4-execution.md commands/review-pr.md commands/work-on.md commands/work-on/remediate.md commands/work-on/review.md docs/WORK-ON-RUNTIME.md scripts/check-spec-markers.sh"
|
|
158
|
+
hold 3520 4977 9 false "concurrency" "concurrency" "bin/engine-cli.mjs bin/engine.mjs bin/engine/runlog.mjs bin/forgedock.mjs bin/runner.mjs bin/tests/engine.test.mjs bin/tests/runner.test.mjs commands/orchestrate/phase-4-execution.md devdocs/modules/runner.md"
|
|
159
|
+
hold 3524 156 5 false "spec-cli" "spec-cli" "bin/engine.mjs bin/engine/phases.mjs bin/runner.mjs bin/tests/engine-phases.test.mjs bin/tests/engine.test.mjs"
|
|
160
|
+
hold 3555 137 8 false "security" "" "bin/engine/phases.mjs bin/tests/engine-phases.test.mjs commands/work-on/build.md commands/work-on/build/implement.md commands/work-on/decompose.md commands/work-on/investigate.md packages/protocol/CHANGELOG.md packages/protocol/src/phases.js"
|
|
161
|
+
|
|
162
|
+
# forge#3862: extended review depth (from the build's review_weight / scope flag) only ever adds reviewers
|
|
163
|
+
SC_TOP="AUTH=0 BILLING=1 CONCURRENCY=0 DATABASE=1 INFRA=0 SECURITY=1 SCRAPING=0 FRONTEND=2 API=2"
|
|
164
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 40 --changed-files 1
|
|
165
|
+
expect_tier "no depth flag stays small" small; expect_sel "no depth flag stays security-only" "security"
|
|
166
|
+
case "$out" in *DEPTH=*) bad "standard roster prints no DEPTH line" ;; *) ok ;; esac
|
|
167
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 40 --changed-files 1 --review-depth STANDARD
|
|
168
|
+
expect_tier "STANDARD depth stays small" small
|
|
169
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 40 --changed-files 1 --review-depth extended
|
|
170
|
+
expect_tier "lowercase depth is not EXTENDED" small
|
|
171
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 40 --changed-files 1 --review-depth EXTENDED
|
|
172
|
+
expect_tier "EXTENDED lifts small to standard" standard; expect_has "EXTENDED keeps security" security
|
|
173
|
+
expect_has "EXTENDED small adds top domains" frontend; expect_has "EXTENDED small adds top domains" api
|
|
174
|
+
case "$out" in *DEPTH=extended*) ok ;; *) bad "extended roster prints DEPTH=extended" ;; esac
|
|
175
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 400 --changed-files 6
|
|
176
|
+
expect_not "standard top-2 omits the third domain" database
|
|
177
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 400 --changed-files 6 --review-depth EXTENDED
|
|
178
|
+
expect_has "EXTENDED standard takes a third domain" database; expect_has "EXTENDED standard keeps security" security
|
|
179
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 30 --fix-pr true --review-depth EXTENDED
|
|
180
|
+
expect_tier "EXTENDED lifts fix-pr to standard" standard
|
|
181
|
+
run "svc/a.py" --scores "$SC_TOP" --changed-lines 400 --thorough true --review-depth EXTENDED
|
|
182
|
+
expect_tier "full stays full" full
|
|
183
|
+
|
|
184
|
+
echo "review-roster: $pass passed, $fail failed"
|
|
185
|
+
[ "$fail" -eq 0 ]
|
|
@@ -19,6 +19,9 @@ set -u
|
|
|
19
19
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
20
20
|
REPO_ROOT=$(cd "$SCRIPT_DIR/.." && pwd)
|
|
21
21
|
cd "$REPO_ROOT" || exit 1
|
|
22
|
+
# Gate scripts emit best-effort telemetry (forge#3834); suites run with it off so stubbed gh calls never buffer into ~/.forge.
|
|
23
|
+
# scripts/telemetry.test.sh sets its own environment.
|
|
24
|
+
export FORGE_TELEMETRY=off
|
|
22
25
|
|
|
23
26
|
TIMEOUT_SECS="${SHELL_TEST_TIMEOUT:-300}"
|
|
24
27
|
# WIRE:PROVEN — manual: SHELL_TEST_TIMEOUT=abc and SHELL_TEST_TIMEOUT=0 both print the error and exit 2
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
# Tests for scripts/lib/telemetry.sh (forge_emit): record shape per event, upsert, rollover, buffer/flush,
|
|
5
|
+
# kill switch, never-fails guarantee. No network: gh is replaced via FORGE_TELEMETRY_GH.
|
|
6
|
+
set -u
|
|
7
|
+
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
8
|
+
LIB="$HERE/lib/telemetry.sh"
|
|
9
|
+
PASS=0; FAIL=0
|
|
10
|
+
ok() { PASS=$((PASS+1)); }
|
|
11
|
+
bad() { FAIL=$((FAIL+1)); echo "FAIL: $1" >&2; }
|
|
12
|
+
check() { if [ "$2" = "$3" ]; then ok; else bad "$1 (got '$2', want '$3')"; fi; }
|
|
13
|
+
|
|
14
|
+
TD=$(mktemp -d); trap 'rm -rf "$TD"' EXIT
|
|
15
|
+
export TD FORGE_REPO=o/r FORGE_ISSUE=5 FORGE_TELEMETRY_BUFFER_DIR="$TD/buf"
|
|
16
|
+
unset FORGE_TELEMETRY FORGE_PR FORGE_RUN FORGE_PHASE FORGE_TELEMETRY_MAX_CHARS
|
|
17
|
+
cat > "$TD/gh" <<'STUB'
|
|
18
|
+
#!/usr/bin/env bash
|
|
19
|
+
echo "$*" >> "$TD/calls"
|
|
20
|
+
case "$*" in
|
|
21
|
+
"api --paginate "*) if [ -f "$TD/comments.json" ]; then cat "$TD/comments.json"; else echo '[]'; fi ;;
|
|
22
|
+
"api -X PATCH"*) b="${@: -1}"; printf '%s' "${b#body=}" > "$TD/patched" ;;
|
|
23
|
+
"api -X POST"*) b="${@: -1}"; printf '%s' "${b#body=}" > "$TD/posted" ;;
|
|
24
|
+
esac
|
|
25
|
+
STUB
|
|
26
|
+
cat > "$TD/gh-fail" <<'STUB'
|
|
27
|
+
#!/usr/bin/env bash
|
|
28
|
+
exit 1
|
|
29
|
+
STUB
|
|
30
|
+
chmod +x "$TD/gh" "$TD/gh-fail"
|
|
31
|
+
export FORGE_TELEMETRY_GH="$TD/gh"
|
|
32
|
+
set_comments() { jq -n --arg b "$1" --arg t "${2:-Bot}" '[{id:77,body:$b,user:{type:$t},author_association:"NONE"}]' > "$TD/comments.json"; }
|
|
33
|
+
nrec() { printf '%s\n' "$1" | grep -c '^{'; }
|
|
34
|
+
reset() { rm -rf "$TD/calls" "$TD/posted" "$TD/patched" "$TD/comments.json" "$TD/buf"; }
|
|
35
|
+
|
|
36
|
+
. "$LIB"
|
|
37
|
+
|
|
38
|
+
# 1. One fixture per event shape: record carries the shared keys plus the payload, typed.
|
|
39
|
+
reset
|
|
40
|
+
forge_emit diff_size lines=10 excluded=2 threshold=0 result=OK
|
|
41
|
+
REC=$(grep '^{' "$TD/posted")
|
|
42
|
+
check "post has marker" "$(head -n 1 "$TD/posted")" "<!-- FORGE:TELEMETRY v1 -->"
|
|
43
|
+
check "shared keys" "$(printf '%s' "$REC" | jq -c 'keys_unsorted[:9]')" '["v","ts","forge_version","repo","issue","pr","run","phase","event"]'
|
|
44
|
+
check "issue number" "$(printf '%s' "$REC" | jq -r '.issue')" "5"
|
|
45
|
+
check "pr null" "$(printf '%s' "$REC" | jq -r '.pr')" "null"
|
|
46
|
+
check "numeric payload" "$(printf '%s' "$REC" | jq -r '.lines + .excluded')" "12"
|
|
47
|
+
check "string payload" "$(printf '%s' "$REC" | jq -r '.result')" "OK"
|
|
48
|
+
for fx in "acceptance_gate phase=review verified=3 failed=0 malformed=1 deferred=0:phase" \
|
|
49
|
+
"finding_disposition severity=HIGH confidence=CONFIRMED agent=auth disposition=ISSUE reason=severity-HIGH:disposition" \
|
|
50
|
+
"review_panel agents_selected=4 agents_completed=4 mode=FULL:mode" \
|
|
51
|
+
"ci_gate result=PASS rc=0 checks=7:checks" \
|
|
52
|
+
"liveness decision=DEFER why=lease:decision"; do
|
|
53
|
+
reset; FORGE_PR=9 forge_emit ${fx%%:*}
|
|
54
|
+
key=${fx##*:}; ev=${fx%% *}
|
|
55
|
+
check "fixture $ev event" "$(grep '^{' "$TD/posted" | jq -r '.event')" "$ev"
|
|
56
|
+
check "fixture $ev key $key" "$(grep '^{' "$TD/posted" | jq -r --arg k "$key" 'has($k)')" "true"
|
|
57
|
+
done
|
|
58
|
+
# review-side events go to the PR, others to the issue
|
|
59
|
+
reset; FORGE_PR=9 forge_emit review_panel mode=FULL; grep -q 'issues/9/comments' "$TD/calls" && ok || bad "review_panel targets PR"
|
|
60
|
+
reset; FORGE_PR=9 forge_emit diff_size lines=1; grep -q 'issues/5/comments' "$TD/calls" && ok || bad "diff_size targets issue"
|
|
61
|
+
|
|
62
|
+
# 2. Sanitising: reserved keys, bad keys, backticks and newlines never reach the record.
|
|
63
|
+
reset
|
|
64
|
+
forge_emit x repo=evil 'a b=1' ok='a`b
|
|
65
|
+
c'
|
|
66
|
+
REC=$(grep '^{' "$TD/posted")
|
|
67
|
+
check "reserved key ignored" "$(printf '%s' "$REC" | jq -r '.repo')" "o/r"
|
|
68
|
+
check "value sanitised" "$(printf '%s' "$REC" | jq -r '.ok')" "a_b c"
|
|
69
|
+
check "bad key dropped" "$(printf '%s' "$REC" | jq 'has("a b")')" "false"
|
|
70
|
+
|
|
71
|
+
# 3. Upsert edits the single trusted comment in place; an untrusted one is ignored.
|
|
72
|
+
reset; forge_emit ci_gate result=PASS; set_comments "$(cat "$TD/posted")"
|
|
73
|
+
forge_emit liveness decision=PROCEED
|
|
74
|
+
check "upsert patches" "$(nrec "$(cat "$TD/patched")")" "2"
|
|
75
|
+
check "no second post" "$(grep -c 'POST' "$TD/calls")" "1"
|
|
76
|
+
reset; forge_emit ci_gate result=PASS; set_comments "$(cat "$TD/posted")" User; rm -f "$TD/posted"
|
|
77
|
+
forge_emit liveness decision=PROCEED
|
|
78
|
+
[ -f "$TD/posted" ] && [ ! -f "$TD/patched" ] && ok || bad "untrusted comment must not be patched"
|
|
79
|
+
|
|
80
|
+
# 4. Rollover to part=N near the size limit.
|
|
81
|
+
reset; forge_emit ci_gate result=PASS; set_comments "$(cat "$TD/posted")"; rm -f "$TD/posted"
|
|
82
|
+
FORGE_TELEMETRY_MAX_CHARS=200 forge_emit liveness decision=PROCEED
|
|
83
|
+
check "rollover header" "$(head -n 1 "$TD/posted")" "<!-- FORGE:TELEMETRY v1 part=2 -->"
|
|
84
|
+
|
|
85
|
+
# 5. Buffer on failure, flush on next emit, nothing lost.
|
|
86
|
+
reset
|
|
87
|
+
FORGE_TELEMETRY_GH="$TD/gh-fail" forge_emit diff_size lines=1
|
|
88
|
+
check "buffered" "$(ls "$TD/buf")" "o_r__5.jsonl"
|
|
89
|
+
forge_emit diff_size lines=2
|
|
90
|
+
check "flushed both records" "$(nrec "$(cat "$TD/posted")")" "2"
|
|
91
|
+
[ ! -e "$TD/buf/o_r__5.jsonl" ] && ok || bad "buffer removed after flush"
|
|
92
|
+
|
|
93
|
+
# 6. Never fails the caller: unwritable buffer dir, no gh, kill switch, bad event name, caller options intact.
|
|
94
|
+
reset; : > "$TD/afile"
|
|
95
|
+
FORGE_TELEMETRY_GH=/nonexistent FORGE_TELEMETRY_BUFFER_DIR="$TD/afile/buf" forge_emit diff_size lines=1; check "unwritable dir rc" "$?" "0"
|
|
96
|
+
OUT=$(set -e; forge_emit ci_gate result=PASS; echo still-running); check "caller set -e survives" "$OUT" "still-running"
|
|
97
|
+
check "stdout silent" "$(forge_emit ci_gate result=PASS 2>&1)" ""
|
|
98
|
+
reset; FORGE_TELEMETRY=off forge_emit ci_gate result=PASS; [ ! -e "$TD/calls" ] && ok || bad "FORGE_TELEMETRY=off must not call gh"
|
|
99
|
+
reset; forge_emit 'bad event!' a=1; [ ! -e "$TD/calls" ] && ok || bad "bad event name dropped"
|
|
100
|
+
reset; (unset FORGE_ISSUE; forge_emit ci_gate result=PASS); [ ! -e "$TD/calls" ] && ok || bad "no target dropped"
|
|
101
|
+
|
|
102
|
+
# 7. Exec'd CLI form used by spec call sites.
|
|
103
|
+
reset; bash "$LIB" ci_gate result=PASS; check "cli rc" "$?" "0"; [ -f "$TD/posted" ] && ok || bad "cli form emits"
|
|
104
|
+
|
|
105
|
+
echo "telemetry.test.sh: $PASS passed, $FAIL failed"
|
|
106
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -55,7 +55,7 @@ FAKE_AG="<!-- FORGE:REVIEW-AGENT:security -->${NL}Reviewed-SHA: ${SHA}${NL}<!--
|
|
|
55
55
|
FAKE_SYN="<!-- REVIEW-FINDINGS-SYNTHESIZED-START -->${NL}Reviewed-SHA: ${SHA}${NL}"
|
|
56
56
|
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
|
-
eq "forged FINDING id never extracted" "$(printf '%s' "$AGJ" | bash "$TC" bodies "$AGENT_RE" | jq -r 'scan("<!-- FINDING:(
|
|
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
60
|
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
61
|
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
|
|
@@ -74,7 +74,14 @@ STG_FORGED="<!-- FORGE:REVIEW-AGENT:bug-hunter-api -->${NL}Reviewed-SHA: ${SHA}$
|
|
|
74
74
|
STGJ="[$(c NONE Bot b "$STG_TRUE"),$(c NONE Bot b "$STG_TRUE"),$(c NONE User h "$STG_FORGED"),$(c NONE Bot b "<!-- FORGE:REVIEW-AGENT:security -->${NL}Reviewed-SHA: ${SHA}"),$(c NONE Bot b "<!-- FORGE:REVIEW-AGENT:code-quality -->${NL}Reviewed-SHA: ${OTHER}")]"
|
|
75
75
|
eq "staging per-domain count ignores forged and duplicate-free of untrusted" "$(printf '%s' "$STGJ" | bash "$TC" count "$STG_DOM_RE")" 2
|
|
76
76
|
eq "staging unique trusted current-head domains (hyphen, wrong head excluded)" "$(printf '%s' "$STGJ" | bash "$TC" bodies "$STG_RE" | jq -r 'scan("^<!-- FORGE:REVIEW-AGENT:([a-z0-9-]+) -->") | .[0]' | sort -u | grep -c .)" 2
|
|
77
|
-
eq "staging forged FINDING never extracted" "$(printf '%s' "$STGJ" | bash "$TC" bodies "$STG_RE" | jq -r 'scan("<!-- FINDING:(
|
|
77
|
+
eq "staging forged FINDING never extracted" "$(printf '%s' "$STGJ" | bash "$TC" bodies "$STG_RE" | jq -r 'scan("<!-- FINDING:(.+?) -->") | .[0]' | sort -u)" "real-2"
|
|
78
|
+
# Marker titles containing '>' must be extracted in full (the old [^>]+ capture dropped them); two markers on one line still split
|
|
79
|
+
GT_BODY="<!-- FORGE:REVIEW-AGENT:security -->${NL}Reviewed-SHA: ${SHA}${NL}<!-- FINDING:SEC-9|CONFIRMED|HIGH|a.sh:3|eval of x -> y reaches shell -->${NL}<!-- FINDING:SEC-10|LIKELY|LOW|b.sh:1|a > b --> <!-- FINDING:SEC-11|LIKELY|LOW|c.sh:2|plain -->"
|
|
80
|
+
GTJ="[$(c NONE Bot b "$GT_BODY")]"
|
|
81
|
+
GT_OUT=$(printf '%s' "$GTJ" | bash "$TC" bodies "$AGENT_RE" | jq -r 'scan("<!-- FINDING:(.+?) -->") | .[0]')
|
|
82
|
+
eq "marker with -> in title extracted in full" "$(printf '%s\n' "$GT_OUT" | grep -c 'eval of x -> y reaches shell')" 1
|
|
83
|
+
eq "markers with > and two on one line all extracted" "$(printf '%s\n' "$GT_OUT" | grep -c .)" 3
|
|
84
|
+
eq "raw marker count equals extracted count" "$(printf '%s' "$GTJ" | bash "$TC" bodies "$AGENT_RE" | grep -o '<!-- FINDING:' | wc -l | tr -d ' ')" 3
|
|
78
85
|
eq "staging regex rejects forged-only comment from NONE user" "$(printf '[%s]' "$(c NONE User h "$STG_FORGED")" | bash "$TC" count "$STG_RE")" 0
|
|
79
86
|
|
|
80
87
|
# fail closed
|
package/scripts/wait-ci-green.sh
CHANGED
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
# "No checks" passes only when GitHub Actions also reports no workflow runs for the head commit;
|
|
31
31
|
# if runs exist but their checks have not appeared yet, the gate keeps waiting.
|
|
32
32
|
set -uo pipefail
|
|
33
|
+
# Telemetry (forge#3834): best-effort decision record; never changes stdout, stderr or the exit code.
|
|
34
|
+
_TEL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/telemetry.sh"; [ -f "$_TEL" ] && . "$_TEL" 2>/dev/null
|
|
35
|
+
type forge_emit >/dev/null 2>&1 || forge_emit() { :; }
|
|
33
36
|
|
|
34
37
|
PR=""; REPO=""
|
|
35
38
|
TIMEOUT="${FORGE_CI_TIMEOUT:-540}"; INTERVAL="${FORGE_CI_INTERVAL:-20}"; SETTLE="${FORGE_CI_SETTLE:-45}"
|
|
@@ -50,7 +53,11 @@ case "$REPO" in */*) ;; *) usage ;; esac
|
|
|
50
53
|
for v in "$TIMEOUT" "$INTERVAL" "$GRACE" "$SETTLE"; do case "$v" in ''|*[!0-9]*) usage ;; esac; done
|
|
51
54
|
[ "$INTERVAL" -ge 1 ] || INTERVAL=1
|
|
52
55
|
|
|
53
|
-
emit() {
|
|
56
|
+
emit() {
|
|
57
|
+
case "$1" in PASS) _rc=0 ;; FAIL) _rc=1 ;; TIMEOUT) _rc=3 ;; *) _rc=2 ;; esac
|
|
58
|
+
FORGE_REPO="${FORGE_REPO:-$REPO}" FORGE_PR="${FORGE_PR:-$PR}" forge_emit ci_gate result="$1" rc="$_rc" checks="${total:-0}"
|
|
59
|
+
echo "CI_GATE: $1"; echo "CI_GATE_HEAD: ${START_SHA:-unknown}"
|
|
60
|
+
}
|
|
54
61
|
head_sha() { gh pr view "$PR" -R "$REPO" --json headRefOid --jq '.headRefOid' 2>/dev/null; }
|
|
55
62
|
START_SHA="$(head_sha)"
|
|
56
63
|
if [ -z "$START_SHA" ]; then START_SHA=""; emit ERROR; echo "cannot read PR #$PR head in $REPO"; exit 2; fi
|
|
@@ -68,7 +68,7 @@ expect "every result names the gated head" "CI_GATE_HEAD: abc1234" "$(printf '%s
|
|
|
68
68
|
|
|
69
69
|
# Static guard: every autonomous `gh pr merge` command line in the pipeline specs must be preceded
|
|
70
70
|
# (within 25 lines) by a CI gate result check. Prose/manual instructions are not command lines.
|
|
71
|
-
for spec in commands/review-pr.md commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
|
|
71
|
+
for spec in commands/review-pr.md scripts/review-finalize.sh commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
|
|
72
72
|
bad=$(awk '
|
|
73
73
|
{ line[NR]=$0 }
|
|
74
74
|
/^[[:space:]]*(MERGE_RESULT=\$\()?gh pr merge / {
|
|
@@ -78,7 +78,7 @@ for spec in commands/review-pr.md commands/work-on/review.md commands/work-on/re
|
|
|
78
78
|
expect "every autonomous merge in $spec is CI-gated" "" "$bad"
|
|
79
79
|
done
|
|
80
80
|
# Every autonomous merge pins the gated head (forge#3187): no merge line may omit --match-head-commit.
|
|
81
|
-
for spec in commands/review-pr.md commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
|
|
81
|
+
for spec in commands/review-pr.md scripts/review-finalize.sh commands/work-on/review.md commands/work-on/remediate.md commands/recover-orphans.md commands/deploy-pr.md; do
|
|
82
82
|
unpinned=$(grep -nE '^[[:space:]]*(MERGE_RESULT=\$\(|MERGE_OUTPUT=\$\()?gh pr merge ' "$HERE/../$spec" | grep -v -- '--match-head-commit')
|
|
83
83
|
expect "every autonomous merge in $spec pins the gated head" "" "$unpinned"
|
|
84
84
|
done
|
|
@@ -90,7 +90,7 @@ if grep -qE '^[[:space:]]*(MERGE_RESULT=\$\()?gh pr merge ' "$HERE/../commands/w
|
|
|
90
90
|
|
|
91
91
|
# Reviewed-head guard: review-pr Phase 8 must refuse to merge a head other than the reviewed commit,
|
|
92
92
|
# and the work-on callers must route "stale review" to a re-review, never a manual merge.
|
|
93
|
-
grep -qF '${GATED_HEAD#"$REVIEWED_SHORT"}' "$HERE/../
|
|
93
|
+
grep -qF '${GATED_HEAD#"$REVIEWED_SHORT"}' "$HERE/../scripts/review-finalize.sh" && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: review-pr Phase 8 (scripts/review-finalize.sh merge-gate) lacks reviewed-head guard"; }
|
|
94
94
|
for f in commands/work-on/review.md; do
|
|
95
95
|
grep -q 'stale review' "$HERE/../$f" && PASS=$((PASS+1)) || { FAILN=$((FAILN+1)); echo "FAIL: $f does not route stale review"; }
|
|
96
96
|
done
|
|
@@ -42,3 +42,12 @@ branches:
|
|
|
42
42
|
default: "main"
|
|
43
43
|
staging: "staging"
|
|
44
44
|
feature_pattern: "milestone/{slug}"
|
|
45
|
+
|
|
46
|
+
# =============================================================================
|
|
47
|
+
# DECOMPOSITION (OPTIONAL)
|
|
48
|
+
# Set close_parent: true to close a decomposed parent issue at decomposition
|
|
49
|
+
# (default false: it stays open until its last child closes).
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
# decomposition:
|
|
53
|
+
# close_parent: false
|