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
package/commands/review-pr.md
CHANGED
|
@@ -353,7 +353,7 @@ Correct code that never runs because another layer blocks, shadows or reroutes i
|
|
|
353
353
|
|
|
354
354
|
### 3A-3B: Risk, scores and roster (computed by `review-prepare.sh`)
|
|
355
355
|
|
|
356
|
-
**Raw line count never lowers review depth, and size never blocks a PR.** Size only ever ADDS depth, through the deterministic `review_weight` the build records in `FORGE:DIFF_SIZE` (forge#3862): a high `review_weight` or an out-of-plan scope flag sets `review_depth: EXTENDED` (read by `review-prepare.sh`, passed to `review-roster.sh --review-depth`; it only ever adds reviewers). The Security agent and the Phase 2 floor are unconditional at every depth and size. The roster is the pure function in `scripts/review-roster.sh`: tiers `full` (`--thorough` or milestone-to-staging), `fix-pr` (`MERGE_ISSUE` carries `review-finding`), `small` (60 lines or fewer, or one file of 150 or fewer: Security only), `medium` (61-150: plus triggers), `standard` (over 150 or unknown: top-2 domains by score plus triggers). Path and contract triggers (CONTRACT `HIGH_RISK`, cross-critical pairs such as auth + billing, churn or danger zone, `bin/engine*`, auth/session, CI/docker/infra, migration and SDK/OpenAPI files) escalate every tier but `full`; BILLING adds Concurrency; **General Security always runs**. <!-- Added: forge#3599 -->
|
|
356
|
+
**Raw line count never lowers review depth, and size never blocks a PR.** Size only ever ADDS depth, through the deterministic `review_weight` the build records in `FORGE:DIFF_SIZE` (forge#3862): a high `review_weight` or an out-of-plan scope flag sets `review_depth: EXTENDED` (read by `review-prepare.sh`, passed to `review-roster.sh --review-depth`; it only ever adds reviewers). The Security agent and the Phase 2 floor are unconditional at every depth and size. The roster is the pure function in `scripts/review-roster.sh`: tiers `full` (`--thorough` or milestone-to-staging), `fix-pr` (`MERGE_ISSUE` carries `review-finding` and the PR is 150 changed lines or fewer with a known size; a larger or unknown-size fix PR follows the size ladder), `small` (60 lines or fewer, or one file of 150 or fewer: Security only), `medium` (61-150: plus triggers), `standard` (over 150 or unknown: top-2 domains by score plus triggers). Path and contract triggers (CONTRACT `HIGH_RISK`, cross-critical pairs such as auth + billing, churn or danger zone, `bin/engine*`, auth/session, CI/docker/infra, migration and SDK/OpenAPI files) escalate every tier but `full`; BILLING adds Concurrency; **General Security always runs**. <!-- Added: forge#3599 -->
|
|
357
357
|
|
|
358
358
|
**Roster label to marker domain (single source of truth)**: the marker domain in `<!-- FORGE:REVIEW-AGENT:{domain} -->` is always the canonical value, never the lowercased roster label, and Phase 4 accepts no alias: `Security`→`security`, `Auth`→`auth`, `Billing`→`billing`, `Concurrency`→`concurrency`, `Database`→`database`, `INFRA`/`Infrastructure`→`infra`, `SCRAPING`/`Scraping`/`Scraper`→`scraper`, `FRONTEND`/`Frontend`/`Web`→`frontend`, `API`→`api` (persona file `<domain>.md`; any other label falls back to lowercase). `SELECTED_AGENTS` from Phase 1 is already canonical. `agent_domain()` is defined inline in every block that needs it and in `scripts/review-roster.sh`; all copies must stay identical (`scripts/review-pr-domain-map.test.sh`). `spec-cli` is ForgeDock-internal: only for diffs touching `bin/**` or `commands/**` in a repo with `commands/review-pr-agents/spec-cli.md`. <!-- Added: forge#3485 -->
|
|
359
359
|
|
|
@@ -367,6 +367,8 @@ Reviewer roster tier \`${ROSTER_TIER}\`: ${ROSTER_REASON}. Roster: ${SELECTED_AG
|
|
|
367
367
|
|
|
368
368
|
**Extended review (`REVIEW_DEPTH=EXTENDED`)** <!-- Added: forge#3862 -->: the roster script has already lifted a small, medium or fix-pr roster to standard and widened the standard baseline to the top-3 domains. Additionally dispatch each selected domain agent once per slice of at most `review.depth.slice_files` changed files (default 12; one slice when the PR has no more files), each slice carrying only its files' diff and the shared PR context, and merge the per-slice findings in Phase 5 (the dedup there treats the same `file:line` from two slices as one). The Security agent always reviews the whole diff, never a slice. Extended depth never skips a Phase 2 check, never weakens the Security floor, and never blocks the merge by itself; it is not a finding.
|
|
369
369
|
|
|
370
|
+
**Degraded gate** <!-- Added: forge#3628 -->: when `MERGE_ISSUE` is set, read the `**Degraded**:` line of its newest trusted `FORGE:QUALITY_GATE` and the `FORGE:ACCEPTANCE_GATE:DEGRADED` list (via `scripts/trusted-comments.sh`). A non-`none` list means a check ran nothing (tests skipped or not run, known-slow skip, blast-radius incomplete). Give each domain agent that list as "Degraded checks to review by hand": it must read the code those checks would have exercised. Post the section in a PR comment whose first line is `<!-- FORGE:REVIEW-AGENT:{domain} -->` (or the synthesis marker) and which carries a `Reviewed-SHA: ${REVIEW_SHA}` line for the current head. Under `### Degraded Paths Reviewed`, write one list item per degraded check as `- <exact name>: <how its path was reviewed>`; a generic section, a stale-head comment, or an item saying the check was NOT reviewed does not satisfy Phase 8 (forge#3995).
|
|
371
|
+
|
|
370
372
|
### 3B.5: Provenance-Based Trust Escalation (Conditional)
|
|
371
373
|
|
|
372
374
|
Proven `(task-type × module-set)` combinations may drop optional judgment agents; novel ones widen to the full affected-domain panel (`NOVEL_NEEDS_HUMAN` also sets `needs-human` on merge). **Hard floor**: Security and every Phase 2 check run at every tier. **Application is skipped** (the lookup still runs, recording the calibration cell key) for `THOROUGH`, milestone-to-staging, `BASE_SYNC_ONLY`, and a `DELTA` head past two full rounds. **Default is SHADOW**: any error path (absent table, parse failure, git error) logs and leaves the roster unchanged, never looser or tighter; `review.trust_shadow_mode: "false"` enforces. `PROVEN` narrows only to Security plus hard-signal agents (AUTH, BILLING, DATABASE or INFRA score >= 3) and never under churn escalation or a danger zone. <!-- forge#1745, forge#1804 -->
|
|
@@ -437,7 +439,7 @@ Re-reviewing a head that only moved does not need the whole panel over the whole
|
|
|
437
439
|
|
|
438
440
|
Phase 2, the §6B.4 gates (full PR diff at the current head) and the 7B/8 gates run unchanged in every mode.
|
|
439
441
|
|
|
440
|
-
**Reviewer-comment trust and shape (applies to every check below and in Phase 4)**: a reviewer comment counts only when it is from a trusted author (`scripts/trusted-comments.sh`, the same predicate as the in-PR fix gate and `verify-phase-trail.sh`: trusted association, Bot account, or `FORGE_TRAIL_TRUSTED_LOGINS`), its body **starts with** `<!-- FORGE:REVIEW-AGENT:{domain} -->`,
|
|
442
|
+
**Reviewer-comment trust and shape (applies to every check below and in Phase 4)**: a reviewer comment counts only when it is from a trusted author (`scripts/trusted-comments.sh`, the same predicate as the in-PR fix gate and `verify-phase-trail.sh`: trusted association, Bot account, or `FORGE_TRAIL_TRUSTED_LOGINS`), its body **starts with** `<!-- FORGE:REVIEW-AGENT:{domain} -->`, it carries a line `Reviewed-SHA: ${REVIEW_SHA}` for the current head, and it carries a `<!-- REVIEW-FINDINGS-START -->` ... `<!-- REVIEW-FINDINGS-END -->` block (empty allowed). A verdict without the block ("APPROVE, no findings") is **undelivered**, never read as clean and never scanned for free-text findings. The head SHA is public, so the SHA line alone proves nothing: an untrusted commenter must never suppress dispatch or satisfy the panel guard. An unresolved trust script or a non-integer count means **not posted** (fail closed).
|
|
441
443
|
|
|
442
444
|
**Pre-dispatch fence.** For `BASE_SYNC_ONLY` it dispatches zero reviewers and posts the carry record (then go straight to Phase 4). Otherwise it implements **idempotent re-entry**: a re-invoked review reuses reviewers that already posted for the current head, so it prints `PENDING_AGENTS` (the domains of the effective roster with no trusted current-SHA comment; comments for another SHA never count) and the linked issue's `BLAST_RADIUS_MANIFEST` (advisory; any failure prints it empty).
|
|
443
445
|
|
|
@@ -479,7 +481,7 @@ agent_domain() { local l; l=$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]');
|
|
|
479
481
|
for AGENT in $SELECTED_AGENTS; do
|
|
480
482
|
AGENT_DOMAIN=$(agent_domain "$AGENT"); HAVE=""
|
|
481
483
|
if [ -n "$TRUSTED_SCRIPT" ]; then
|
|
482
|
-
HAVE=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "
|
|
484
|
+
HAVE=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${REVIEW_SHA}(\\r?\\n|$)" 2>/dev/null || echo "")
|
|
483
485
|
fi
|
|
484
486
|
case "$HAVE" in ''|*[!0-9]*) HAVE=0 ;; esac # fail closed: unreadable or untrusted counts as not posted
|
|
485
487
|
[ "$HAVE" -lt 1 ] && PENDING_AGENTS="${PENDING_AGENTS} ${AGENT}"
|
|
@@ -548,7 +550,7 @@ Each agent's `[FILE_LIST]` is `slices.<domain>.paths` from the context: the Secu
|
|
|
548
550
|
|
|
549
551
|
## Phase 4: Wait for Agents
|
|
550
552
|
|
|
551
|
-
Completion is read from GitHub, never from notifications. A reviewer is complete only when a **trusted** comment (3C trust rule) starting with `<!-- FORGE:REVIEW-AGENT:{domain} -->` carries `Reviewed-SHA: <current head
|
|
553
|
+
Completion is read from GitHub, never from notifications. A reviewer is complete only when a **trusted** comment (3C trust rule) starting with `<!-- FORGE:REVIEW-AGENT:{domain} -->` carries `Reviewed-SHA: <current head>` and a findings block (`REVIEW-FINDINGS-START` ... `REVIEW-FINDINGS-END`, empty allowed). A domain whose comment lacks the block is missing: re-dispatch it **once** (3C, with the note "structured findings block required: end your comment with REVIEW-FINDINGS-START/END, empty if no findings"), then re-run this phase; a second miss is a delivery failure and takes the existing degraded-panel hard stop below. Never loop beyond that one re-dispatch.
|
|
552
554
|
|
|
553
555
|
- **Joined tools** (`Task`, `task`): the reviewers already returned, so one pass decides; anything missing is a delivery failure.
|
|
554
556
|
- **Async `Agent`**: bounded wait, **one poll pass per Bash call** (a call is capped at 120s, 600s max, so one long loop would be killed before the hard stop ran). The first pass records an epoch deadline in a private absolute file keyed by PR and head SHA; every exit removes it and 3C removes it before (re)dispatch, so a re-review never inherits an expired window. Re-run while it prints `PANEL_STATUS: WAITING`.
|
|
@@ -590,7 +592,7 @@ agent_domain() { local l; l=$(printf '%s' "${1}" | tr '[:upper:]' '[:lower:]');
|
|
|
590
592
|
for AGENT in $SELECTED_AGENTS; do
|
|
591
593
|
AGENT_DOMAIN=$(agent_domain "$AGENT"); N=""
|
|
592
594
|
if [ -n "$TRUSTED_SCRIPT" ] && [ -n "$COMMENTS_JSON" ]; then
|
|
593
|
-
N=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "
|
|
595
|
+
N=$(printf '%s' "$COMMENTS_JSON" | bash "$TRUSTED_SCRIPT" count "^(?=[\\s\\S]*<!-- REVIEW-FINDINGS-START -->[\\s\\S]*<!-- REVIEW-FINDINGS-END -->)<!-- FORGE:REVIEW-AGENT:${AGENT_DOMAIN} -->[\\s\\S]*(^|\\n)Reviewed-SHA: ${REVIEW_SHA}(\\r?\\n|$)" 2>/dev/null || echo "")
|
|
594
596
|
fi
|
|
595
597
|
case "$N" in ''|*[!0-9]*) N=0 ;; esac # fail closed
|
|
596
598
|
if [ "$N" -lt 1 ]; then MISSING_AGENT_COMMENTS="${MISSING_AGENT_COMMENTS} ${AGENT_DOMAIN}"; else ACTUAL_AGENT_COUNT=$(( ACTUAL_AGENT_COUNT + 1 )); fi
|
|
@@ -655,7 +657,7 @@ cat "$OUT_DIR/note-list.txt" 2>/dev/null
|
|
|
655
657
|
|
|
656
658
|
`extract` reads the trusted synthesized block for the head, else its trusted agent bodies (`BASE_SYNC_ONLY`: the carried head; `DELTA`: this head plus the earlier reviewed one), parses every `<!-- FINDING:... -->` marker (an unparseable marker blocks the review), dedupes by file + line range ±5 and by title (3+ shared keywords, higher confidence wins), and applies the **§6B.4 gates** in order, each keeping the finding when its input is unreadable: (1) **head-SHA**: a cited line past the end of the file at `REVIEW_SHA` is stale and DROPPED (`findings_dropped_stale`); (2) **introduced-line**: a MEDIUM-or-lower finding whose range (±5) misses the PR's added lines is `pre-existing` (**CRITICAL/HIGH are never routed as pre-existing**, whatever their confidence, `protocols.md` §5; input comes from the diff, never a reviewer's `**Scope**` tag; deleted, renamed and patch-less files always intersect); (3) **concrete-failure**: a MEDIUM-or-lower finding without a `**Failure scenario**` becomes a NOTE (`notes_demoted_no_scenario`).
|
|
657
659
|
|
|
658
|
-
Read `STRUCTURED_FINDINGS`, `FINDINGS_KEPT`, `FINDINGS_DROPPED_STALE`, `FINDINGS_PREEXISTING` and `NOTES_DEMOTED_NO_SCENARIO` for the record and summary. The caller's part of 6A: add the Phase 2.5 `INTEG-N` findings no reviewer covered (CONFIRMED HIGH, so ISSUE, filed in 6C);
|
|
660
|
+
Read `STRUCTURED_FINDINGS`, `FINDINGS_KEPT`, `FINDINGS_DROPPED_STALE`, `FINDINGS_PREEXISTING` and `NOTES_DEMOTED_NO_SCENARIO` for the record and summary. The caller's part of 6A: add the Phase 2.5 `INTEG-N` findings no reviewer covered (CONFIRMED HIGH, so ISSUE, filed in 6C); findings come only from `FINDING:` markers, never from free text. If `AGENTS_WITHOUT_BLOCK` is non-empty those agents are undelivered: re-dispatch them once (Phase 4 rule), else take the degraded-panel path; `CLEAN_STRUCTURED` is withheld while it is non-empty. On `CLEAN_STRUCTURED=true` and no `INTEG-N` the review is clean: skip to Phase 7. A finding that somehow has no marker is classified with `classify-finding.sh --unstructured` (empty or unparseable severity gives `NOTE unstructured`, never ISSUE). **Pre-existing route**: a pre-existing finding (MEDIUM or lower) is never filed as `review-finding`/`needs-validation` nor linked to this PR (it stays out of findings-per-PR and the amplification ratio); it still passes the §6B.5 classifier, is listed once in the summary, and at most 3 per review (highest severity first) are filed with the `pre-existing` label plus `priority:*`, citing this PR (`preexisting_filed`, not `findings_filed`).
|
|
659
661
|
|
|
660
662
|
### 6B.5: Non-blocking note disposition (MANDATORY before 6C)
|
|
661
663
|
|
|
@@ -665,7 +667,7 @@ A standalone issue per LOW/POSSIBLE note makes the cascade amplify (each merged
|
|
|
665
667
|
- **Review-finding lineage** (`MERGE_ISSUE` carries `review-finding`: this PR fixes a finding): only MEDIUM findings that are CONFIRMED, or LIKELY and safety-exempt, become issues; the rest are NOTEs. A MEDIUM tagged `**Fix efficacy**: fails|regresses` (the linked issue is not fixed) is never damped.
|
|
666
668
|
- **Cross-head re-flag** (`NOTE reflag-*`): a finding an earlier head's trusted review or `FORGE:NOTE_DISPOSITION` already covered is not filed again; never for HIGH/CRITICAL or rows whose linked issue is closed.
|
|
667
669
|
- **Per-review cap**: at most `review.max_filed_findings` ISSUEs (default 2; 1 under lineage; `0` valid), ranked by severity, confidence, safety domain; the overflow is `NOTE capped` (listed, never filed or dropped). HIGH/CRITICAL are never capped and still block via §7B.
|
|
668
|
-
- **Contract scope**: a finding under a verified `deferred → #N` item of the trusted FORGE:CONTRACT (`scripts/check-contract-scope.sh list`) is `NOTE contract-deferred #N` (commented there, not filed), under `accepted-risk` it is `NOTE contract-accepted-risk`; never for HIGH/CRITICAL, `not-affected`, safety-exempt or this PR's own files, nor on missing, untrusted or closed contract input.
|
|
670
|
+
- **Contract scope**: a finding under a verified `deferred → #N` item (verified = open non-PR issue by a trusted author with the `FORGE:DEFERRED_FROM` marker and an exact checklist line `- [ ] \`path\`` for that path, never a body substring) of the trusted FORGE:CONTRACT (`scripts/check-contract-scope.sh list`) is `NOTE contract-deferred #N` (commented there, not filed; counted only after #N is still OPEN and the comment succeeded, otherwise the finding takes the disposition it would have had absent the contract, normally filed, a LOW/POSSIBLE one stays a NOTE), under `accepted-risk` it is `NOTE contract-accepted-risk`; never for HIGH/CRITICAL, `not-affected`, safety-exempt or this PR's own files, nor on missing, untrusted or closed contract input.
|
|
669
671
|
|
|
670
672
|
Each NOTE gets one disposition: (1) **fix in this PR**, only comment, documentation or test-only changes as a follow-up commit (any executable change is filed or re-reviewed in full; never push unreviewed code under `--auto-merge`); (2) **list in the PR body** under `## Non-blocking notes` (edit, never replace; `mktemp` file named for the PR number, read back to confirm); (3) **drop** duplicates, stale-comment nits and speculation. NOTES never reach the filing call. The 6A dry run printed the NOTE list: decide the fixed and dropped counts (`NOTES_FIXED`, `NOTES_DROPPED`, default 0; other plain NOTEs count as listed), then the 6C fence runs `classify` for real.
|
|
671
673
|
|
|
@@ -674,7 +676,7 @@ Each NOTE gets one disposition: (1) **fix in this PR**, only comment, documentat
|
|
|
674
676
|
|
|
675
677
|
### 6B.6: In-PR fix gate
|
|
676
678
|
|
|
677
|
-
A CONFIRMED MEDIUM-or-higher defect in a file this PR changed is cheaper to fix before merge than through a separate `/work-on` cycle: `classify` marks it `INPR_FIX` and posts the `FORGE:INPR_FIX` request after the disposition marker (`INPR_FIX_POSTED=true`), so do NOT file those findings in 6C. Narrow by design: auto-merge reviews with `--issue`, files in the PR diff, one round per PR (any trusted `FORGE:INPR_FIX*` marker ends it). Phase 8 then refuses to merge this head (blocker `in-pr fix required`). If the round fails, `/work-on` waives it for MEDIUM and files the findings (`work-on/review.md` R4); HIGH/CRITICAL are never waived and end at `needs-human`.
|
|
679
|
+
A CONFIRMED MEDIUM-or-higher defect, or a LIKELY MEDIUM defect whose line the PR introduced (`scope=introduced`), in a file this PR changed is cheaper to fix before merge than through a separate `/work-on` cycle: `classify` marks it `INPR_FIX` and posts the `FORGE:INPR_FIX` request after the disposition marker (`INPR_FIX_POSTED=true`), so do NOT file those findings in 6C. `review-finalize.sh extract` computes `scope` (`introduced` | `pre-existing` | `unknown`) per finding from the PR diff (the added-line window; unknown provenance never widens the gate). Narrow by design: auto-merge reviews with `--issue`, files in the PR diff, one round per PR (any trusted `FORGE:INPR_FIX*` marker ends it). Phase 8 then refuses to merge this head (blocker `in-pr fix required`). If the round fails, `/work-on` waives it for MEDIUM and files the findings (`work-on/review.md` R4); HIGH/CRITICAL are never waived and end at `needs-human`.
|
|
678
680
|
|
|
679
681
|
### 6C: Create Issues
|
|
680
682
|
|
|
@@ -718,6 +720,7 @@ cat <<'ISSUE_EOF' > "$FINDING_ISSUE_BODY_FILE"
|
|
|
718
720
|
**Agent**: [name] ([domain])
|
|
719
721
|
**Confidence**: [CONFIRMED/LIKELY/POSSIBLE]
|
|
720
722
|
**Severity**: [CRITICAL/HIGH/MEDIUM/LOW]
|
|
723
|
+
**Scope**: introduced|pre-existing (the worklist item's `scope`; omit the line when it is `unknown`)
|
|
721
724
|
**Review comment**: [permalink to agent comment]
|
|
722
725
|
|
|
723
726
|
## Pattern Metadata
|
|
@@ -799,25 +802,30 @@ Append a `## Review Findings` table (summaries and issue links) to the PR body:
|
|
|
799
802
|
|
|
800
803
|
```bash
|
|
801
804
|
PR_NUMBER="{PR_NUMBER}"; REPO="{GH_REPO}"; MERGE_ISSUE="{MERGE_ISSUE}"
|
|
802
|
-
MERGE_HEALTH="{MERGE_HEALTH}"; MERGE_HEALTH_STATE="{MERGE_HEALTH_STATE}"; BASE="{BASE}"; HEAD="{HEAD}"; REVIEW_SHA="{REVIEW_SHA}"
|
|
805
|
+
MERGE_HEALTH="{MERGE_HEALTH}"; MERGE_HEALTH_STATE="{MERGE_HEALTH_STATE}"; BASE="{BASE}"; HEAD="{HEAD}"; REVIEW_SHA="{REVIEW_SHA}"; CTX="{CONTEXT_FILE}"
|
|
803
806
|
INTENSITY_TIER="{INTENSITY_TIER}"; TABLE_CELL_KEY="{TABLE_CELL_KEY}"; TRUST_SHADOW_MODE="{TRUST_SHADOW_MODE}"; TRUST_NEEDS_HUMAN="{TRUST_NEEDS_HUMAN}"
|
|
804
807
|
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; 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/*; 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() { :; }; }
|
|
805
808
|
RF=$(forge_resolve review-finalize.sh) # install roots only, never the PR checkout; unresolvable is a stop (fail closed), never a skipped gate
|
|
806
809
|
[ -n "$RF" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: scripts/review-finalize.sh not resolvable (set FORGEDOCK_HOME to the ForgeDock install); refusing to continue (fail closed)"; exit 1; } # STOP
|
|
807
|
-
|
|
810
|
+
OUT_DIR="$(dirname "$CTX")/review-finalize-${PR_NUMBER}"; mkdir -p "$OUT_DIR"
|
|
811
|
+
# Free text (purpose-regression findings, agents list) derives from PR content: BEFORE this fence, write it with the file-write tool (never a
|
|
812
|
+
# shell heredoc, echo or printf of the text) to "$OUT_DIR/purpose-findings.txt" (ids and reasons; leave absent when there is no purpose regression)
|
|
813
|
+
# and "$OUT_DIR/agents.txt" ("{N} agents: {names}"). It is passed by path, so it is never re-parsed as shell. Never put free text in a shell word.
|
|
814
|
+
# --result: stale (PR HEAD moved) | clean (no blocking criteria) | blocking (any criterion above).
|
|
808
815
|
VERDICT_OUT=$(bash "$RF" verdict --pr "$PR_NUMBER" --repo "$REPO" --review-sha "$REVIEW_SHA" --base "$BASE" --head-branch "$HEAD" \
|
|
809
816
|
--merge-health "$MERGE_HEALTH" --merge-health-state "$MERGE_HEALTH_STATE" --is-milestone "{IS_MILESTONE_TO_STAGING}" \
|
|
810
|
-
--has-purpose-regression "{HAS_PURPOSE_REGRESSION}" --purpose-findings
|
|
811
|
-
--result "{RESULT}" --agents
|
|
812
|
-
--intensity-tier "$INTENSITY_TIER" --table-cell-key "$TABLE_CELL_KEY" --trust-shadow-mode "$TRUST_SHADOW_MODE"
|
|
817
|
+
--has-purpose-regression "{HAS_PURPOSE_REGRESSION}" --purpose-findings-file "$OUT_DIR/purpose-findings.txt" \
|
|
818
|
+
--result "{RESULT}" --agents-file "$OUT_DIR/agents.txt" --findings-count "{M}" --blocking-count "{N_BLOCKING}" \
|
|
819
|
+
--intensity-tier "$INTENSITY_TIER" --table-cell-key "$TABLE_CELL_KEY" --trust-shadow-mode "$TRUST_SHADOW_MODE" \
|
|
820
|
+
--other-blocking "{OTHER_BLOCKING}" --out-dir "$OUT_DIR"); VERDICT_RC=$?
|
|
813
821
|
printf '%s\n' "$VERDICT_OUT"
|
|
814
822
|
[ "$VERDICT_RC" -eq 0 ] || exit 1 # STOP: REVIEW_RESULT: status: BLOCKED was printed
|
|
815
823
|
# 7B.5: reads task type and confidence from the linked issue's FORGE:INVESTIGATOR, consults origin/forge-knowledge, records FORGE:CALIBRATION_CHECK.
|
|
816
|
-
bash "$RF" calibrate --pr "$PR_NUMBER" --repo "$REPO" --issue "$MERGE_ISSUE" \
|
|
824
|
+
bash "$RF" calibrate --pr "$PR_NUMBER" --repo "$REPO" --issue "$MERGE_ISSUE" --out-dir "$OUT_DIR" \
|
|
817
825
|
--intensity-tier "$INTENSITY_TIER" --table-cell-key "$TABLE_CELL_KEY" --trust-shadow-mode "$TRUST_SHADOW_MODE" --trust-needs-human "$TRUST_NEEDS_HUMAN"
|
|
818
826
|
```
|
|
819
827
|
|
|
820
|
-
`verdict` computes `BASE_CONFLICT` (the single source of truth for the Phase 8 guards), posts the verdict with the trust annotation, and never posts a clean verdict over a base conflict or purpose regression (it downgrades to CHANGES REQUESTED, `VERDICT_COERCED=true`). Read `VERDICT=`, `BASE_CONFLICT=`, `HAS_PURPOSE_REGRESSION=` and, from `calibrate`, `CALIBRATION_NEEDS_HUMAN=` / `CALIBRATION_NOTE=`: they are the Phase 8 flags.
|
|
828
|
+
`verdict` computes `BASE_CONFLICT` (the single source of truth for the Phase 8 guards), posts the verdict with the trust annotation, and never posts a clean verdict over a base conflict or purpose regression (it downgrades to CHANGES REQUESTED, `VERDICT_COERCED=true`). `{OTHER_BLOCKING}` is `true` when criterion 1, 2, 3 or 5 fired, else `false`; `verdict` forces it `true` for a blocking result that the base conflict and purpose regression do not explain, persists the resolved value to `$OUT_DIR/other-blocking.txt` and prints `OTHER_BLOCKING=`, and `merge-gate` ORs the persisted value in, so the reviewer cannot lower it. Read `VERDICT=`, `BASE_CONFLICT=`, `HAS_PURPOSE_REGRESSION=`, `OTHER_BLOCKING=` and, from `calibrate`, `CALIBRATION_NEEDS_HUMAN=` / `CALIBRATION_NOTE=`: they are the Phase 8 flags. The note itself is also written to `$OUT_DIR/calibration-note.txt`, which Phase 8 passes by path; free text is never placed in a shell word.
|
|
821
829
|
|
|
822
830
|
---
|
|
823
831
|
|
|
@@ -825,19 +833,21 @@ bash "$RF" calibrate --pr "$PR_NUMBER" --repo "$REPO" --issue "$MERGE_ISSUE" \
|
|
|
825
833
|
|
|
826
834
|
**Skip if** `AUTO_MERGE=false`.
|
|
827
835
|
|
|
828
|
-
**One script owns every Phase 8 gate (MANDATORY; a gate is never skipped, reordered or hand-run).** `review-finalize.sh merge-gate` runs, in order, and stops at the first refusal: the note-disposition preflight (a trusted `FORGE:NOTE_DISPOSITION` must exist), the in-PR fix gate (an unanswered trusted `FORGE:INPR_FIX` at the current head blocks), the phase-trail preflight (`scripts/verify-phase-trail.sh` on the linked issue; `--issue` is mandatory and never inferred; an infrastructure failure is `BLOCKED` plus `needs-human`) <!-- Added: forge#3061 -->, the guard-state flags below (`VERDICT` is `APPROVED` or `CHANGES REQUESTED`, the others `true`/`false`; an empty or unsubstituted value aborts with `needs-human`; `--other-blocking true` when 7B criterion 1, 2, 3 or 5 fired), routing (a base conflict as the ONLY blocker routes to the base-sync path, `blocker: base-conflict`; any other hold adds `needs-human`), the pre-merge mergeability guard on fresh state, the previously-escalated guard (parks at `workflow:awaiting-merge`, never merges), the CI gate (`scripts/wait-ci-green.sh`) with the reviewed-head guard (only the commit the `FORGE:REVIEW_ROUTE` sha approved; a docs-only delta is allowed), and finally the merge with `--match-head-commit`. Every gate fails closed.
|
|
836
|
+
**One script owns every Phase 8 gate (MANDATORY; a gate is never skipped, reordered or hand-run).** `review-finalize.sh merge-gate` runs, in order, and stops at the first refusal: the note-disposition preflight (a trusted `FORGE:NOTE_DISPOSITION` must exist), the in-PR fix gate (an unanswered trusted `FORGE:INPR_FIX` at the current head blocks), the phase-trail preflight (`scripts/verify-phase-trail.sh` on the linked issue; `--issue` is mandatory and never inferred; an infrastructure failure is `BLOCKED` plus `needs-human`) <!-- Added: forge#3061 -->, the degraded-gate review (`degraded-gate-unreviewed` unless a `### Degraded Paths Reviewed` section covers each degraded check), the guard-state flags below (`VERDICT` is `APPROVED` or `CHANGES REQUESTED`, the others `true`/`false`; an empty or unsubstituted value aborts with `needs-human`; `--other-blocking true` when 7B criterion 1, 2, 3 or 5 fired), routing (a base conflict as the ONLY blocker routes to the base-sync path, `blocker: base-conflict`; any other hold adds `needs-human`), the pre-merge mergeability guard on fresh state, the previously-escalated guard (parks at `workflow:awaiting-merge`, never merges), the CI gate (`scripts/wait-ci-green.sh`) with the reviewed-head guard (only the commit the `FORGE:REVIEW_ROUTE` sha approved; a docs-only delta is allowed), and finally the merge with `--match-head-commit`. Every gate fails closed.
|
|
829
837
|
|
|
830
838
|
```bash
|
|
831
839
|
PR_NUMBER="{PR_NUMBER}"; REPO="{GH_REPO}"
|
|
832
840
|
MERGE_ISSUE="{MERGE_ISSUE}"; MERGE_BASE="{MERGE_BASE}"; IS_MILESTONE_TO_STAGING="{IS_MILESTONE_TO_STAGING}"
|
|
833
|
-
VERDICT="{VERDICT}"; HAS_PURPOSE_REGRESSION="{HAS_PURPOSE_REGRESSION}"; CALIBRATION_NEEDS_HUMAN="{CALIBRATION_NEEDS_HUMAN}";
|
|
841
|
+
VERDICT="{VERDICT}"; HAS_PURPOSE_REGRESSION="{HAS_PURPOSE_REGRESSION}"; CALIBRATION_NEEDS_HUMAN="{CALIBRATION_NEEDS_HUMAN}"; CTX="{CONTEXT_FILE}"
|
|
834
842
|
TRUST_NEEDS_HUMAN="{TRUST_NEEDS_HUMAN}"; BASE_CONFLICT="{BASE_CONFLICT}"; OTHER_BLOCKING="{OTHER_BLOCKING}"; TABLE_CELL_KEY="{TABLE_CELL_KEY}"
|
|
835
843
|
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; 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/*; 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() { :; }; }
|
|
836
844
|
RF=$(forge_resolve review-finalize.sh) # install roots only, never the PR checkout; unresolvable is a stop (fail closed), never a skipped gate
|
|
837
845
|
[ -n "$RF" ] || { echo "REVIEW_RESULT: status: BLOCKED, blocker: scripts/review-finalize.sh not resolvable (set FORGEDOCK_HOME to the ForgeDock install); refusing to continue (fail closed)"; exit 1; } # STOP
|
|
846
|
+
# The calibration note derives from issue text: `calibrate` (Phase 7B.5) wrote it to this file; it is passed by path, never substituted into a shell word.
|
|
847
|
+
OUT_DIR="$(dirname "$CTX")/review-finalize-${PR_NUMBER}"
|
|
838
848
|
GATE_OUT=$(bash "$RF" merge-gate --pr "$PR_NUMBER" --repo "$REPO" --issue "$MERGE_ISSUE" --base "$MERGE_BASE" \
|
|
839
|
-
--verdict "$VERDICT" --has-purpose-regression "$HAS_PURPOSE_REGRESSION" --calibration-needs-human "$CALIBRATION_NEEDS_HUMAN" --calibration-note "$
|
|
840
|
-
--trust-needs-human "$TRUST_NEEDS_HUMAN" --base-conflict "$BASE_CONFLICT" --other-blocking "$OTHER_BLOCKING" --table-cell-key "$TABLE_CELL_KEY"); GATE_RC=$?
|
|
849
|
+
--verdict "$VERDICT" --has-purpose-regression "$HAS_PURPOSE_REGRESSION" --calibration-needs-human "$CALIBRATION_NEEDS_HUMAN" --calibration-note-file "$OUT_DIR/calibration-note.txt" \
|
|
850
|
+
--trust-needs-human "$TRUST_NEEDS_HUMAN" --base-conflict "$BASE_CONFLICT" --other-blocking "$OTHER_BLOCKING" --table-cell-key "$TABLE_CELL_KEY" --out-dir "$OUT_DIR"); GATE_RC=$?
|
|
841
851
|
printf '%s\n' "$GATE_OUT"
|
|
842
852
|
# 8B: only for a milestone->staging PR whose merge just succeeded: clears the milestone on its open review-finding issues so they flow through the fast lane.
|
|
843
853
|
if [ "$GATE_RC" -eq 0 ] && printf '%s\n' "$GATE_OUT" | grep -q '^MERGE_STATE=MERGED$'; then
|
|
@@ -848,7 +858,8 @@ exit "$GATE_RC"
|
|
|
848
858
|
|
|
849
859
|
**Outcome mapping.** Report the script's output; never re-run its gates by hand and never `gh pr merge` yourself.
|
|
850
860
|
- Exit 0, `MERGE_OUTCOME=merged` (`MERGE_STATE=MERGED`): merged. `merge-failed`: the merge did not land (the script commented on the issue). `parked-awaiting-merge`: parked per the previously-escalated guard; the script performs the transition itself (`transition-label.sh ... awaiting-merge`, prose fallback `gh issue edit --add-label "workflow:awaiting-merge"`).
|
|
851
|
-
- Exit 3: print the `REVIEW_RESULT: status: BLOCKED, blocker: ...` line and stop; `MERGE_OUTCOME=` names the case (`base-conflict`, `needs-human`, `protection-blocked`, `stale-review`, `ci-not-green`). A CI gate TIMEOUT prints `CI_GATE_TIMEOUT`: re-run the same fence up to 3 more times. A phase-trail blocker with no MISSING lines (`phase trail unreadable (rc=N)`, `auto-merge requires --issue`) is an infrastructure or usage failure: callers neither re-run phases nor merge manually.
|
|
861
|
+
- Exit 3: print the `REVIEW_RESULT: status: BLOCKED, blocker: ...` line and stop; `MERGE_OUTCOME=` names the case (`base-conflict`, `needs-human`, `protection-blocked`, `stale-review`, `ci-not-green`). A `base-conflict` blocker carries no label (the autonomous base sync is pipeline work, not a human decision), so every caller of `--auto-merge` MUST map it explicitly: `work-on:review` R4, the router's Phase 0A.1 re-review fallback and `orchestrate` re-review Step 3 each set `workflow:remediating` (falling back to `needs-human`), and a standalone caller runs `/work-on <issue> --remediate`. A caller never leaves the issue unlabelled at `workflow:in-review`. A CI gate TIMEOUT prints `CI_GATE_TIMEOUT`: re-run the same fence up to 3 more times. A phase-trail blocker with no MISSING lines (`phase trail unreadable (rc=N)`, `auto-merge requires --issue`) is an infrastructure or usage failure: callers neither re-run phases nor merge manually.
|
|
862
|
+
- Exit 3 with `blocker: degraded-gate-unreviewed` (`needs-human` added): the linked issue's gate is DEGRADED and no trusted review comment for the current head has a `### Degraded Paths Reviewed` section with a `- name: how reviewed` item for every degraded check. Not a code defect: `work-on:review` (R4) re-runs the review once so the section is added. Never merge by hand. <!-- Added: forge#3628 -->
|
|
852
863
|
- Exit 4: `REVIEW_RESULT: status: PHASE_TRAIL_FAILED` plus the `MISSING:` lines. Return that status and stop: this phase never re-runs the missing phases and never escalates to `needs-human`; `work-on:review` (R4) alone owns re-dispatching them (forge#3102).
|
|
853
864
|
|
|
854
865
|
Phase 8 ONLY merges the PR. It does NOT close the issue, update labels or clean up worktrees: when invoked via `/work-on` those belong to `work-on/close.md`, and doing them here would close the issue with `workflow:merged` before the close phase ran. <!-- Added: forge#815 -->
|
|
@@ -83,7 +83,7 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
|
83
83
|
Before doing any work, select the architect comments with ONE anchored, trusted selector (body must start with the marker; author must be a Bot or OWNER/MEMBER/COLLABORATOR; mirrors `scripts/trusted-comments.sh`). A comment that merely mentions the marker (an INVESTIGATOR report, a CONTEXT briefing) never matches. <!-- Added: forge#3648 -->
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
ARCH=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments | jq -s '[.[][] | select((.body // "") | startswith("<!-- FORGE:ARCHITECT -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:ARCHITECT:COMPLETE -->")), other: (.body | test("
|
|
86
|
+
ARCH=$(gh api --paginate repos/{GH_REPO}/issues/{NUMBER}/comments | jq -s '[.[][] | select((.body // "") | startswith("<!-- FORGE:ARCHITECT -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:ARCHITECT:COMPLETE -->")), other: (.body | test("\\n[ \\t]*<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|BUILDER)"))}]') || ARCH=""
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
- If `ARCH` is empty (unparsable) → fail closed: delete nothing and treat as no plan.
|
|
@@ -604,6 +604,39 @@ The block is delimited by `<!-- FORGE:BLAST_RADIUS:BEGIN -->` and `<!-- FORGE:BL
|
|
|
604
604
|
|
|
605
605
|
---
|
|
606
606
|
|
|
607
|
+
## Phase A2.9: Append-Only Registry Check *(skip when no planned file is a hand-maintained list)* <!-- Added: forge#3969 -->
|
|
608
|
+
|
|
609
|
+
Two PRs appending at the same spot of a shared list (test paths, cron targets, workflow steps) conflict textually although every line is independent, and each conflict costs a base-sync remediation round. For each planned file that is an append-only registry (one independent entry per line, edited by many PRs):
|
|
610
|
+
|
|
611
|
+
1. Confirm with `bash <scripts>/detect-append-registries.sh --detect` (history ranking) and `--check-union <path>`. Resolve `<scripts>` through the trusted script resolution (plugin root, `FORGE_ROOT`, `FORGEDOCK_HOME`), never `$PWD`. The helper rejects JSON, YAML, TOML, lockfiles and Markdown.
|
|
612
|
+
2. Plan one of: (a) a `.gitattributes` `merge=union` entry, only when `--check-union` prints `UNION SAFE` and every line is independent (no ordering, uniqueness or structure); or (b) converting the list to glob discovery where its consumer supports it.
|
|
613
|
+
3. Name the registry and the chosen option in `### Affected Paths`. Never plan union for a file with ordering, uniqueness or structure: that file is edited at the end of the list, and the pre-handoff base sync resolves its conflicts.
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Phase A2.10: Consumer Sweep Intake *(advisory: skip silently when no existing identifier changes; never blocks)* <!-- Added: forge#3970 -->
|
|
618
|
+
|
|
619
|
+
No diff exists yet, so sweep by name. List the existing identifiers the plan will change, rename or re-type (functions, classes, constants, config or context keys, CLI flags, env vars, paths, literal hashes or versions), take them from the investigation's affected files, and run the script in identifier mode. Validate (Phase V4.6) later re-runs the same sweep on the final diff and gates it.
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
# FORGE_ROOT: shared resolver scripts/lib/forge-resolve.sh (empty when unresolved => the sweep is skipped, never guessed)
|
|
623
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; 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/*; 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() { :; }; }
|
|
624
|
+
FORGE_ROOT="$(forge_root 2>/dev/null || true)"
|
|
625
|
+
CS_SCRIPT="${FORGE_ROOT:+$FORGE_ROOT/scripts/consumer-sweep.sh}"; [ -f "$CS_SCRIPT" ] || CS_SCRIPT=""
|
|
626
|
+
EXCLUDES=()
|
|
627
|
+
while IFS= read -r _g; do [ -n "$_g" ] && [ "$_g" != "null" ] && EXCLUDES+=(--exclude-glob "$_g"); done \
|
|
628
|
+
< <(yq '(.build.scope.exclude_globs // []) + (.build.diff_size.exclude_globs // []) | .[]' forge.yaml 2>/dev/null || true)
|
|
629
|
+
if [ -z "$CS_SCRIPT" ]; then echo "CONSUMER_SWEEP: unavailable (script not found under FORGE_ROOT)"; else
|
|
630
|
+
bash "$CS_SCRIPT" --ident {IDENTIFIER_1} --ident {IDENTIFIER_2} ${EXCLUDES[@]+"${EXCLUDES[@]}"}; echo "CONSUMER_SWEEP_RC=$?"
|
|
631
|
+
fi
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Each identifier must match `^[A-Za-z0-9_.:/-]{4,80}$` (flags without leading dashes). Output is TSV `identifier<TAB>kind<TAB>file:line`; a `truncated` row means more sites exist than the cap shows (advisory). Exit `2` or an unavailable script is not "no sites": write "consumer sweep unavailable" in `### Risk Assessment` and continue; never block the plan.
|
|
635
|
+
|
|
636
|
+
Fold the result into Phase A2.8, with no new record grammar: each swept identifier becomes a `SYMBOL:` (`query` = the identifier) and each referencing file a `HIT:` (`disposition=change` when the plan edits it, else `verified-unaffected` with a reason after reading the site). The builder must update or justify each site (implement I6 `CONSUMERS:` block).
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
607
640
|
## Phase A3: Consistency Rules
|
|
608
641
|
|
|
609
642
|
Identify invariants that ALL affected paths must satisfy. These become the builder's consistency checklist.
|
|
@@ -636,6 +669,14 @@ Rules for ordering:
|
|
|
636
669
|
|
|
637
670
|
---
|
|
638
671
|
|
|
672
|
+
## Phase A4.5: Edge-Case Enumeration *(skip on the skip-marker path; an empty block is valid)* <!-- Added: forge#3631 -->
|
|
673
|
+
|
|
674
|
+
List the error, edge and concurrency cases the change must survive, so validate (Phase V4.7, `scripts/check-edge-cases.sh`) can require a test for each. Cover every error class the code path can raise (not only the one in the report), boundary and empty inputs, and concurrent or repeated invocation. Skip cases that only restate a normal-path acceptance criterion.
|
|
675
|
+
|
|
676
|
+
Record one single-line record per case between `<!-- FORGE:EDGE_CASES:BEGIN -->` and `<!-- FORGE:EDGE_CASES:END -->`: `EDGE: id=e1 kind=error|edge|concurrency description=<free text>`. Ids are `e1`, `e2`, ... and are unique and stable. Only `id=` and `kind=` are parsed; the description is free text. Emit an empty block (or omit the section) when the change has no such cases. The builder maps each id to a test or an `untestable=` reason in FORGE:BUILDER (implement I6). Never use the architect marker strings as delimiters.
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
639
680
|
## Phase A5: Risk Assessment
|
|
640
681
|
|
|
641
682
|
For each non-obvious interaction or potential failure mode:
|
|
@@ -744,6 +785,16 @@ HIT: symbol=s1 file={FILE} role={producer|consumer|sibling} disposition=change
|
|
|
744
785
|
HIT: symbol=s1 file={FILE} role={producer|consumer|sibling} disposition=verified-unaffected reason={one-hyphenated-token}
|
|
745
786
|
<!-- FORGE:BLAST_RADIUS:END -->
|
|
746
787
|
|
|
788
|
+
### Edge Cases
|
|
789
|
+
<!-- Phase A4.5. Human table plus the machine block. Omit when skipped; an empty block is valid. -->
|
|
790
|
+
| id | kind | description |
|
|
791
|
+
|----|------|-------------|
|
|
792
|
+
| e1 | {error\|edge\|concurrency} | {DESCRIPTION} |
|
|
793
|
+
|
|
794
|
+
<!-- FORGE:EDGE_CASES:BEGIN -->
|
|
795
|
+
EDGE: id=e1 kind={error|edge|concurrency} description={ONE_LINE_DESCRIPTION}
|
|
796
|
+
<!-- FORGE:EDGE_CASES:END -->
|
|
797
|
+
|
|
747
798
|
### Implementation Order
|
|
748
799
|
1. {FIRST_CHANGE} — {WHY_FIRST}
|
|
749
800
|
2. {SECOND_CHANGE} — {WHY_SECOND}
|
|
@@ -852,8 +903,10 @@ B4 → [THIS MODULE] Architecture Planning (FORGE:ARCHITECT comment)
|
|
|
852
903
|
Phase A2.5: Pipeline Phase-Dependency Check
|
|
853
904
|
Phase A2.7: Pattern Sweep Intake (review-finding issues only)
|
|
854
905
|
Phase A2.8: Blast-Radius Manifest (enforced by validate V4.5, passed to review-pr)
|
|
906
|
+
Phase A2.10: Consumer Sweep Intake (consumer-sweep.sh --ident; gated by validate V4.6)
|
|
855
907
|
Phase A3: Consistency Rules
|
|
856
908
|
Phase A4: Sequence the Implementation
|
|
909
|
+
Phase A4.5: Edge-Case Enumeration (EDGE: ids; gated by validate V4.7)
|
|
857
910
|
Phase A5: Risk Assessment
|
|
858
911
|
B5 → Implement (builder reads plan, implements ALL affected paths)
|
|
859
912
|
```
|
|
@@ -42,7 +42,9 @@ Parse from $ARGUMENTS:
|
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
+
- `--fix-consumers "<UNADDRESSED lines>"` — optional. Set only by build B6 after validate returned `consumer_unaddressed`. Skip I1–I2 planning: for each listed `UNADDRESSED: <identifier> <file:line>` site, either update that file so it no longer depends on the old identifier (the file then joins the change set) or add a `- <path>[:<line>] unaffected: <reason>` line to the `### Consumers` block of the FORGE:BUILDER comment (the match is by path, split on the literal ` unaffected: `, so any path characters work). Never edit the checker (`scripts/consumer-sweep.sh`) or its stop-lists, and never add `unaffected:` lines without reading the site. Stage the changes, return `IMPLEMENT_RESULT: status: COMPLETE`; if a site cannot be addressed, leave it: build records it as `CONSUMERS: unresolved`. This is the single repair round; there is no second one. <!-- Added: forge#4017 -->
|
|
45
46
|
- `--fix-acceptance "<failed checks>"` — optional. Set only by build B6.5 after a failed acceptance gate. In this mode, skip I1–I2 planning: read the listed failing `ACCEPTANCE_CHECK` ids from the FORGE:INVESTIGATOR comment, change the code in `{WORKTREE_PATH}` until each check's target/matcher holds (do not edit the checks), update the BUILDER comment's `### Changes` list, and return `IMPLEMENT_RESULT: status: COMPLETE`. If a check is impossible as written (contradicts the contract), return `status: BLOCKED` with that reason. **Decomposition refusal** <!-- Added: forge#3543 -->: never implement work that the FORGE:INVESTIGATOR `### Decomposition Assessment` (`**YES**`) assigns to a separate sub-issue, even if a listed check targets it; return `status: BLOCKED` with blocker "check <id> belongs to decomposed item <N>" instead.
|
|
47
|
+
- `--fix-selfreview "<findings>"` — optional. Set only by build B5.6 after the build-time self-review returned CONFIRMED or LIKELY findings at MEDIUM or above. <!-- Added: forge#3971 --> Skip I1–I2 planning. `<findings>` is one line per finding (`SR-<n> <CONFIDENCE> <SEVERITY> <file:line> <summary>`). For each: read the cited code, fix it in `{WORKTREE_PATH}` (the file joins the change set), or decline it when it is wrong or out of the contract's scope. Never edit a test, check or fixture to silence a finding, never touch the reviewer's comments, and add no unrequested scope. Re-read every pattern-sweep sibling of a fixed defect. Stage the changes and update the FORGE:BUILDER comment's `### Changes` list and its `### Self-review` section (see I6): one `fixed:` line per fixed finding and one `declined: <id> <reason>` line per declined one, with a real reason. Return `IMPLEMENT_RESULT: status: COMPLETE`. This is the single repair round, build guards it with `FORGE:SELFREVIEW_REPAIR`; there is no second one. If a finding cannot be addressed without contradicting the contract, decline it with that reason rather than returning BLOCKED.
|
|
46
48
|
|
|
47
49
|
## Phase I1: Load Context from GitHub
|
|
48
50
|
|
|
@@ -92,7 +94,7 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
|
92
94
|
Select with ONE anchored, trusted selector (body must start with the marker; author must be a Bot or OWNER/MEMBER/COLLABORATOR; mirrors `scripts/trusted-comments.sh`). A comment that merely mentions the marker (INVESTIGATOR, CONTEXT, ARCHITECT) never matches. Delete ONLY when exactly one non-complete candidate exists and it carries no other FORGE phase marker; unparsable, zero, or multiple matches delete nothing (fail closed). <!-- Added: forge#3668 -->
|
|
93
95
|
```bash
|
|
94
96
|
# only a current-cycle partial is ever deleted; prior-cycle comments are never touched
|
|
95
|
-
BLD=$(printf '%s' "$CYCLE_SLICE" | jq -s '[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("
|
|
97
|
+
BLD=$(printf '%s' "$CYCLE_SLICE" | jq -s '[.[][] | select((.body // "") | startswith("<!-- FORGE:BUILDER -->")) | select((.user.type // "") == "Bot" or ((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR"))) | {id: .id, complete: (.body | contains("<!-- FORGE:BUILDER:COMPLETE -->")), other: (.body | test("\\n[ \\t]*<!-- FORGE:(INVESTIGATOR|CONTEXT|CONTRACT|ARCHITECT)"))}]') || BLD=""
|
|
96
98
|
PARTIAL_ID=$(printf '%s' "$BLD" | jq -r '[.[] | select(.complete | not)] | if length == 1 and (.[0].other | not) then .[0].id else empty end' 2>/dev/null)
|
|
97
99
|
if [ -n "$PARTIAL_ID" ]; then
|
|
98
100
|
gh api repos/{GH_REPO}/issues/comments/$PARTIAL_ID -X DELETE
|
|
@@ -173,6 +175,8 @@ Work in `{WORKTREE_PATH}`. Follow the contract deliverables table exactly — im
|
|
|
173
175
|
- **Deliverable-type consistency check**: Before committing, compare the actual output against the CONTRACT's deliverable list. If the CONTRACT explicitly states "no code changes required", "docs only", or "configuration update only" AND the diff introduces new executable files (`.py`, `.js`, `.ts`, `.sh` — not test, config, or documentation files), STOP. Do NOT commit. Re-read the CONTRACT, the investigation recommendation, and the ARCHITECT plan. If all three agree that code is needed, update the CONTRACT comment to reflect the new deliverable type before proceeding. If only the builder decided to add code without contract support, discard the code change and implement the contracted deliverable instead. <!-- Added: forge#279 -->
|
|
174
176
|
- **Pipeline check documentation — generalization rule**: When writing or updating pipeline check documentation (in `commands/*.md`), describe the **bug class**, not a specific incident. Do NOT embed: PR numbers, issue numbers, run IDs, timestamps, function names, dollar amounts, or multi-sentence incident timelines in check prose or `**Evidence**:` blocks. One brief HTML comment `<!-- Added: forge#NNN -->` is acceptable per check for traceability. `**Evidence**:` blocks must describe the vulnerability pattern (what the class of bug looks like, why it's dangerous) — not narrate a single historical occurrence. CHANGELOG entries may reference originating issues; command prompt text must not.
|
|
175
177
|
- **Pattern sweep — class-wide fix and test**: When the FORGE:ARCHITECT plan or FORGE:INVESTIGATOR report contains a `### Pattern Sweep` table (`Query | Hit | Disposition`), fix EVERY hit with disposition `fix`, not only the instance the reviewer cited. Write the test against a representative set of instances from the table, not the reviewer's single repro, so it fails for any sibling still carrying the defect. Do not tune a rule, matcher, or fixture to the cited example. If a `fix` row cannot be changed, or a `not-affected` row turns out to be affected, say so in the `### Approach` of the FORGE:BUILDER comment instead of dropping it silently. Absent table = no-op. <!-- Added: forge#3449 -->
|
|
178
|
+
- **Consumer sweep — address every referencing site**: when the FORGE:ARCHITECT plan lists consumer-sweep sites (`HIT:` rows from `scripts/consumer-sweep.sh`, Phase A2.10), update each referencing file or justify it as unaffected. Validate (V4.6) re-runs the sweep on the final diff; in `blocking` mode it returns `consumer_unaddressed` for every referencing site outside the change set that has no justification, and build runs one repair round (`--fix-consumers`). Sites still unaddressed after that round are recorded as `CONSUMERS: unresolved` and passed to review as known risk; they never block. Record the justifications in the `CONSUMERS:` block of the FORGE:BUILDER comment (Phase I6). <!-- Added: forge#3970 -->
|
|
179
|
+
- **Edge cases — one test per id**: when the FORGE:ARCHITECT plan carries a `FORGE:EDGE_CASES` block (`### Edge Cases`, Phase A4.5), write one test per `EDGE:` id (error, edge and concurrency cases, not only the happy path) and keep the test in the diff's added lines. Record each id in the `### Edge Case Coverage` section of the FORGE:BUILDER comment (Phase I6). Validate (V4.7) fails with `edge-cases: <ids>` for an id with no `EDGE_TEST:` line, a mapped test name absent from the added lines of its file, or an empty `untestable=` reason. Use `untestable=<reason>` only when no test can exercise the case, and give the real reason. An empty or absent block is a no-op. <!-- Added: forge#3631 -->
|
|
176
180
|
- **Endpoint response contract consumer tracing**: When changing an endpoint's response body shape or status field values (e.g., changing `"status": "healthy"` to `"status": "ok"`, renaming response keys, removing fields), grep the full repo for ALL consumers of that response body before committing. Consumers are not limited to the service being changed — they include deploy scripts, CI health checks, monitoring configs, docker-compose healthcheck definitions, Traefik probes, and any script that parses or pattern-matches on the response body. Run: `grep -rn "{old_value}\|{endpoint_path}" scripts/ infra/ .github/ docker-compose*.yml traefik/ 2>/dev/null`. All consumers whose behavior depends on the old response format MUST be updated in the same PR — a response contract change that updates only one consumer while leaving others on the old format is a deploy-time breakage. <!-- Added: forge#321 -->
|
|
177
181
|
- If you discover the contract is wrong (e.g. a file doesn't exist, a function has a different signature): STOP, post a comment on the issue explaining the discrepancy, add label `needs-human`, and EXIT with `IMPLEMENT_RESULT: status: BLOCKED` and a `blocker` describing the discrepancy
|
|
178
182
|
|
|
@@ -355,6 +359,29 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:BUILDER -->
|
|
|
355
359
|
### Acceptance Criteria Status
|
|
356
360
|
{Checklist of each criterion from the contract, marked ✅ or ❌}
|
|
357
361
|
|
|
362
|
+
### Consumers
|
|
363
|
+
CONSUMERS:
|
|
364
|
+
- {path}[:{line}] unaffected: {one-line reason}
|
|
365
|
+
- {path} updated
|
|
366
|
+
|
|
367
|
+
CONSUMERS: unresolved
|
|
368
|
+
- {path}:{line} unresolved: {identifier}
|
|
369
|
+
|
|
370
|
+
> **Consumers block**: one line per referencing site of a changed or removed identifier (the architect's A2.10 sweep and the V4.6 `UNADDRESSED:` output). Only `unaffected: <reason>` lines justify a site outside the change set; the match is by file path (the line number is optional, lines shift). A file you changed needs no line. `unresolved` lines are written by validate V5 for sites left unaddressed after the one repair round (a recorded known risk for review): they never justify a site and you do not write them by hand. Omit the block when there are no sites.
|
|
371
|
+
|
|
372
|
+
### Edge Case Coverage
|
|
373
|
+
EDGE_TEST: id=e1 test={path}::{test_name}
|
|
374
|
+
EDGE_TEST: id=e2 untestable={reason}
|
|
375
|
+
|
|
376
|
+
> **Edge case block**: one line per `EDGE:` id of the architect's `FORGE:EDGE_CASES` block, each line starting at the line start (no prose before `EDGE_TEST:`). `test=` names a file and a test name (a single token with no whitespace; name the test with underscores or hyphens) that occurs in a line this change adds to that file; a pre-existing test does not count. `untestable=` needs a non-empty reason. Omit the section when the plan has no edge cases.
|
|
377
|
+
|
|
378
|
+
### Self-review
|
|
379
|
+
SELFREVIEW:
|
|
380
|
+
- fixed: {SR-id} {file:line} {one-line what changed}
|
|
381
|
+
- declined: {SR-id} {file:line} {reason}
|
|
382
|
+
|
|
383
|
+
> **Self-review block** (written only by a `--fix-selfreview` round, forge#3971): one line per finding build B5.6 handed over. `declined:` lines carry the reason so the official `/review-pr` sees the finding as known; a declined finding is not a defect to hide, it is a decision to review. Omit the section when no self-review round ran.
|
|
384
|
+
|
|
358
385
|
### Testing Checklist
|
|
359
386
|
- [ ] {Test scenario 1} [type:api]
|
|
360
387
|
- [ ] {Test scenario 2} [type:unit]
|