forgedock 1.4.0 → 1.5.1
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/runlog.mjs +11 -1
- package/bin/engine-cli.mjs +6 -0
- package/bin/engine.mjs +194 -59
- package/bin/forgedock.mjs +112 -5
- package/bin/hooks/pre-tool-use.mjs +29 -7
- package/bin/journey.mjs +790 -193
- package/bin/runner.mjs +159 -2
- package/bin/watch-utils.mjs +4 -1
- package/commands/cleanup.md +27 -2
- package/commands/issue.md +14 -4
- package/commands/orchestrate/config.md +3 -1
- package/commands/orchestrate/phase-1-resolve.md +125 -14
- package/commands/orchestrate/phase-3-dependency.md +254 -40
- package/commands/orchestrate/phase-4-execution.md +402 -39
- package/commands/orchestrate/phase-5-cleanup.md +2 -0
- package/commands/review-pr-staging.md +51 -8
- package/commands/review-pr.md +97 -32
- package/commands/work-on/build/architect.md +28 -3
- package/commands/work-on/remediate.md +53 -4
- package/package.json +1 -1
- package/packages/protocol/src/validate.js +24 -1
- package/scripts/build-knowledge-index.mjs +6 -2
- package/scripts/check-phase-registry-drift.mjs +23 -1
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/classify-lane.sh +29 -3
- package/scripts/derive-finding-milestone.sh +194 -0
- package/scripts/doctor-pipeline-state.sh +1 -0
- package/scripts/extract-affected-files.sh +232 -0
- package/scripts/extract-affected-files.test.sh +331 -0
- package/scripts/forge-run.sh +4 -0
- package/scripts/issue-dedup.sh +69 -8
- package/scripts/issue-dedup.test.sh +142 -0
- package/scripts/severity-to-priority.sh +81 -0
|
@@ -681,14 +681,32 @@ gh label create "needs-validation" --color "FBCA04" --description "Review findin
|
|
|
681
681
|
gh label create "staging-review" --color "1D76DB" --description "Finding from a staging branch review before deploy to main. Managed by ForgeDock." --force -R {GH_REPO} 2>/dev/null
|
|
682
682
|
```
|
|
683
683
|
|
|
684
|
-
### 7D: Milestone Detection
|
|
685
|
-
|
|
684
|
+
### 7D: Milestone & Code Branch Detection
|
|
685
|
+
|
|
686
|
+
Derive both `CODE_BRANCH` and `MILESTONE_FLAG` from the reviewed PR before creating any findings. Unlike `commands/review-pr.md` (feature-lane PRs with short-lived HEAD branches that get deleted post-merge — see the rationale comment on that spec's finding template), `review-pr-staging.md` reviews long-lived branches (`staging`, `milestone/*`) that persist after merge, so `CODE_BRANCH` uses `headRefName` (the PR's HEAD — the branch actually being reviewed), consistent with forge#1391's original fix. This replaces the previous hardcoded `staging` literal, which was silently wrong whenever this spec reviewed a `milestone/X → staging` PR instead of a `staging → main` PR — `CODE_BRANCH` used to always say `staging` even when the reviewed branch was `milestone/X`.
|
|
687
|
+
|
|
688
|
+
```bash
|
|
689
|
+
CODE_BRANCH=$(gh pr view ${PR_NUMBER} --json headRefName --jq '.headRefName')
|
|
690
|
+
MILESTONE_FLAG=""
|
|
691
|
+
|
|
692
|
+
# scripts/derive-finding-milestone.sh is the single source of truth for
|
|
693
|
+
# milestone derivation — commands/review-pr.md and commands/review-pr-staging.md
|
|
694
|
+
# both call it identically so the two specs cannot independently drift.
|
|
695
|
+
# <!-- forge#2443 -->
|
|
696
|
+
MILESTONE_TITLE=$(bash scripts/derive-finding-milestone.sh "${PR_NUMBER}" -R {GH_REPO})
|
|
697
|
+
# Quoted so multi-word milestone titles survive as a single value wherever
|
|
698
|
+
# MILESTONE_FLAG is later interpolated — matches the quoting convention already
|
|
699
|
+
# used for --title/--body-file/--label below.
|
|
700
|
+
[ -n "$MILESTONE_TITLE" ] && MILESTONE_FLAG="--milestone \"$MILESTONE_TITLE\""
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Plain staging→main reviews resolve `CODE_BRANCH` to `staging` dynamically (same value as before, now derived rather than hardcoded) and typically resolve no milestone. A milestone→staging review resolves `CODE_BRANCH` to `milestone/X` and (via the shared script) the matching milestone.
|
|
686
704
|
|
|
687
705
|
### 7E: Deduplicate Against Existing Issues
|
|
688
706
|
Check for open review-finding issues at same file:line → skip. Closed issues at same location → potential regression (elevate priority).
|
|
689
707
|
|
|
690
708
|
### 7F: Create Issues
|
|
691
|
-
Sequential creation. Title: `Staging Review: {summary} (staging → main)`. Labels: review-finding, needs-validation, staging-review, priority:
|
|
709
|
+
Sequential creation. Title: `Staging Review: {summary} (staging → main)`. Labels: review-finding, needs-validation, staging-review, priority:P0-P3 (derived from Severity — see below). Body includes: source branch context (`${CODE_BRANCH}`, derived in Phase 7D — not a hardcoded `staging` literal), code context, evidence, validation checklist.
|
|
692
710
|
|
|
693
711
|
**For each finding** (that passes dedup), create issue through the `/issue` create-hook's programmatic invocation contract (see `commands/issue.md` § "Programmatic Invocation Contract") instead of calling the raw issue-creation command directly:
|
|
694
712
|
```bash
|
|
@@ -716,10 +734,10 @@ Files that need changes:
|
|
|
716
734
|
|
|
717
735
|
## Source Branch Context
|
|
718
736
|
|
|
719
|
-
**Code branch**: `
|
|
720
|
-
**Worktree base**: `origin/
|
|
737
|
+
**Code branch**: `[CODE_BRANCH]`
|
|
738
|
+
**Worktree base**: `origin/[CODE_BRANCH]`
|
|
721
739
|
|
|
722
|
-
> When fixing: `git worktree add ../fix-{slug} -b fix/{slug} origin/
|
|
740
|
+
> When fixing: `git worktree add ../fix-{slug} -b fix/{slug} origin/[CODE_BRANCH]`
|
|
723
741
|
|
|
724
742
|
## Code Context
|
|
725
743
|
[10 lines around finding]
|
|
@@ -733,8 +751,32 @@ Files that need changes:
|
|
|
733
751
|
- [ ] If VALIDATED: fix implemented and tested on correct branch
|
|
734
752
|
ISSUE_EOF
|
|
735
753
|
|
|
754
|
+
# STAGING_FINDING_SEVERITY is extracted from the finding's own **Severity**
|
|
755
|
+
# body field (set above in the heredoc) — example assignment shown here for
|
|
756
|
+
# clarity, same convention as STAGING_FINDING_TITLE etc.
|
|
757
|
+
STAGING_FINDING_SEVERITY="LOW"
|
|
758
|
+
|
|
759
|
+
# priority:* label is a deterministic function of the finding's **Severity**
|
|
760
|
+
# (CRITICAL/HIGH/MEDIUM/LOW) — NEVER of its Confidence
|
|
761
|
+
# (CONFIRMED/LIKELY/POSSIBLE). scripts/severity-to-priority.sh is the single
|
|
762
|
+
# source of truth for this mapping; commands/review-pr.md calls the identical
|
|
763
|
+
# script so the two specs cannot independently drift. <!-- forge#2447 -->
|
|
764
|
+
STAGING_FINDING_PRIORITY=$(bash scripts/severity-to-priority.sh "$STAGING_FINDING_SEVERITY")
|
|
765
|
+
STAGING_FINDING_PRIORITY_EXIT=$?
|
|
766
|
+
|
|
767
|
+
# Exit code MUST be checked before use — severity-to-priority.sh exits 1 (empty stdout)
|
|
768
|
+
# on a missing/unrecognized severity. Proceeding with an empty $STAGING_FINDING_PRIORITY
|
|
769
|
+
# would call Skill(issue, --label "") instead of aborting issue creation for this finding.
|
|
770
|
+
# Mirrors the identical check in commands/review-pr.md. <!-- forge#2479 -->
|
|
771
|
+
if [ "$STAGING_FINDING_PRIORITY_EXIT" -ne 0 ]; then
|
|
772
|
+
echo "PRIORITY: severity-to-priority.sh failed (exit $STAGING_FINDING_PRIORITY_EXIT) for severity '$STAGING_FINDING_SEVERITY' — skipping finding issue creation"
|
|
773
|
+
# Skip this finding — do NOT fall through to issue creation with an empty label
|
|
774
|
+
else
|
|
775
|
+
|
|
736
776
|
# --label is repeatable (not comma-joined) per the /issue programmatic contract.
|
|
737
|
-
|
|
777
|
+
# ${MILESTONE_FLAG} carries the Phase 7D derivation through — empty string is
|
|
778
|
+
# a no-op arg when the reviewed branch has no milestone (plain staging→main).
|
|
779
|
+
Skill(skill="issue", args="--title \"$STAGING_FINDING_TITLE\" --body-file \"$STAGING_FINDING_BODY_FILE\" --label review-finding --label needs-validation --label staging-review --label \"$STAGING_FINDING_PRIORITY\" ${MILESTONE_FLAG}")
|
|
738
780
|
rm -f "$STAGING_FINDING_BODY_FILE"
|
|
739
781
|
|
|
740
782
|
# /issue has no machine-readable return contract — resolve the created issue's number by
|
|
@@ -746,9 +788,10 @@ for _resolve_attempt in 1 2 3; do
|
|
|
746
788
|
[ -n "$ISSUE_NUM" ] && break
|
|
747
789
|
sleep 2
|
|
748
790
|
done
|
|
791
|
+
fi
|
|
749
792
|
```
|
|
750
793
|
|
|
751
|
-
Labels: `review-finding` + `needs-validation` + `staging-review` + priority (`priority:P1`
|
|
794
|
+
Labels: `review-finding` + `needs-validation` + `staging-review` + priority. `priority:*` is derived from the finding's `**Severity**` field via `scripts/severity-to-priority.sh` (identical script used by `commands/review-pr.md` — single documented mapping, see that script's header comment): `CRITICAL` → `priority:P0`, `HIGH` → `priority:P1`, `MEDIUM` → `priority:P2`, `LOW` → `priority:P3`. **Never derive `priority:*` from Confidence** (CONFIRMED/LIKELY/POSSIBLE) — conflating the two axes previously mislabeled LOW-severity CONFIRMED findings as `priority:P1`, defeating orchestrate's P3 batching rule. <!-- forge#2447 -->
|
|
752
795
|
|
|
753
796
|
**No pre-filtering**: Every finding becomes an issue. Validation agents sort out false positives downstream.
|
|
754
797
|
|
package/commands/review-pr.md
CHANGED
|
@@ -1626,26 +1626,28 @@ gh label create "false-positive" --color "CCCCCC" --description "Review finding
|
|
|
1626
1626
|
|
|
1627
1627
|
**Milestone detection:**
|
|
1628
1628
|
```bash
|
|
1629
|
-
#
|
|
1630
|
-
#
|
|
1629
|
+
# BASE_BRANCH is what the finding template stamps as **Code branch** below
|
|
1630
|
+
# (see the rationale comment at that template block) — HEAD_BRANCH is no
|
|
1631
|
+
# longer needed here; derive-finding-milestone.sh resolves its own
|
|
1632
|
+
# base/head branch lookup internally for its Tier 3 slug match.
|
|
1631
1633
|
BASE_BRANCH=$(gh pr view ${PR_NUMBER} --json baseRefName --jq '.baseRefName')
|
|
1632
|
-
HEAD_BRANCH=$(gh pr view ${PR_NUMBER} --json headRefName --jq '.headRefName')
|
|
1633
1634
|
MILESTONE_FLAG=""
|
|
1634
1635
|
|
|
1635
|
-
#
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1636
|
+
# scripts/derive-finding-milestone.sh is the single source of truth for
|
|
1637
|
+
# milestone derivation — commands/review-pr.md and commands/review-pr-staging.md
|
|
1638
|
+
# both call it identically so the two specs cannot independently drift the
|
|
1639
|
+
# way they did before this fix (forge#2443 Instance B/C: findings from a
|
|
1640
|
+
# milestone-lane PR sometimes inherited the milestone and sometimes didn't,
|
|
1641
|
+
# depending on incidental inheritance rather than an explicit step). The
|
|
1642
|
+
# script's 3-tier resolution (PR's own milestone -> originating issue's
|
|
1643
|
+
# milestone via Closes/Fixes/Resolves #N -> branch-slug match against
|
|
1644
|
+
# milestone/* branches) supersedes the old base/head-branch-only slug match
|
|
1645
|
+
# that used to be hand-rolled here.
|
|
1646
|
+
MILESTONE_TITLE=$(bash scripts/derive-finding-milestone.sh "${PR_NUMBER}" -R "${REPO}")
|
|
1647
|
+
# Quoted so multi-word milestone titles (e.g. "Watch & Fleet Observability") survive
|
|
1648
|
+
# as a single value wherever MILESTONE_FLAG is later interpolated — matches the
|
|
1649
|
+
# quoting convention already used for --title/--body-file/--label below.
|
|
1650
|
+
[ -n "$MILESTONE_TITLE" ] && MILESTONE_FLAG="--milestone \"$MILESTONE_TITLE\""
|
|
1649
1651
|
```
|
|
1650
1652
|
|
|
1651
1653
|
**Dedup against existing issues (MANDATORY before creating):**
|
|
@@ -1763,10 +1765,26 @@ Files that need changes:
|
|
|
1763
1765
|
|
|
1764
1766
|
## Source Branch Context
|
|
1765
1767
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1768
|
+
<!--
|
|
1769
|
+
BASE_BRANCH, not HEAD_BRANCH, is correct here (forge#2443). This looks like
|
|
1770
|
+
a revert of forge#1391 ("review-finding template stamps BASE_BRANCH instead
|
|
1771
|
+
of HEAD_BRANCH — fixes target wrong code state") but is NOT: forge#1391's
|
|
1772
|
+
scenario was a staging->main review where HEAD (staging) is a long-lived
|
|
1773
|
+
branch that already had the bug's code, while BASE (main) had not yet
|
|
1774
|
+
received it. review-pr.md's scenario is different — HEAD here is a
|
|
1775
|
+
short-lived feat/*|fix/* branch that Phase 6E deletes once the auto-merge
|
|
1776
|
+
step completes, which happens SYNCHRONOUSLY within this same /review-pr run,
|
|
1777
|
+
before any downstream consumer (a human, or /work-on) ever reads this
|
|
1778
|
+
finding. By the time the finding is read, HEAD no longer exists but BASE
|
|
1779
|
+
does, and BASE now contains the merged code the finding is about.
|
|
1780
|
+
commands/review-pr-staging.md keeps HEAD_BRANCH semantics (as a dynamic
|
|
1781
|
+
${CODE_BRANCH}, not a hardcoded literal) because ITS reviewed branches
|
|
1782
|
+
(staging, milestone/*) are long-lived — consistent with forge#1391.
|
|
1783
|
+
-->
|
|
1784
|
+
**Code branch**: `[BASE_BRANCH]`
|
|
1785
|
+
**Worktree base**: `origin/[BASE_BRANCH]`
|
|
1786
|
+
|
|
1787
|
+
> When fixing: `git worktree add ../fix-{slug} -b fix/{slug} origin/[BASE_BRANCH]`
|
|
1770
1788
|
|
|
1771
1789
|
## Code Context
|
|
1772
1790
|
[10 lines around finding]
|
|
@@ -1785,8 +1803,30 @@ Files that need changes:
|
|
|
1785
1803
|
[BATCHABLE_ANNOTATION]
|
|
1786
1804
|
ISSUE_EOF
|
|
1787
1805
|
|
|
1806
|
+
# FINDING_SEVERITY is extracted from the finding's own **Severity** body field
|
|
1807
|
+
# (set above in the heredoc) — example assignment shown here for clarity, same
|
|
1808
|
+
# convention as FINDING_ISSUE_TITLE etc.
|
|
1809
|
+
FINDING_SEVERITY="LOW"
|
|
1810
|
+
|
|
1811
|
+
# priority:* label is a deterministic function of the finding's **Severity**
|
|
1812
|
+
# (CRITICAL/HIGH/MEDIUM/LOW) — NEVER of its Confidence
|
|
1813
|
+
# (CONFIRMED/LIKELY/POSSIBLE). scripts/severity-to-priority.sh is the single
|
|
1814
|
+
# source of truth for this mapping; commands/review-pr-staging.md calls the
|
|
1815
|
+
# identical script so the two specs cannot independently drift. <!-- forge#2447 -->
|
|
1816
|
+
FINDING_PRIORITY=$(bash scripts/severity-to-priority.sh "$FINDING_SEVERITY")
|
|
1817
|
+
FINDING_PRIORITY_EXIT=$?
|
|
1818
|
+
|
|
1819
|
+
# Exit code MUST be checked before use — severity-to-priority.sh exits 1 (empty stdout)
|
|
1820
|
+
# on a missing/unrecognized severity, mirroring the DEDUP_EXIT idiom above. Proceeding
|
|
1821
|
+
# with an empty $FINDING_PRIORITY would call Skill(issue, --label "") instead of aborting
|
|
1822
|
+
# issue creation for this finding. <!-- forge#2479 -->
|
|
1823
|
+
if [ "$FINDING_PRIORITY_EXIT" -ne 0 ]; then
|
|
1824
|
+
echo "PRIORITY: severity-to-priority.sh failed (exit $FINDING_PRIORITY_EXIT) for severity '$FINDING_SEVERITY' — skipping finding issue creation"
|
|
1825
|
+
# Skip this finding — do NOT fall through to issue creation with an empty label
|
|
1826
|
+
else
|
|
1827
|
+
|
|
1788
1828
|
# --label is repeatable (not comma-joined) per the /issue programmatic contract.
|
|
1789
|
-
Skill(skill="issue", args="--title \"$FINDING_ISSUE_TITLE\" --body-file \"$FINDING_ISSUE_BODY_FILE\" --label review-finding --label needs-validation --label \"
|
|
1829
|
+
Skill(skill="issue", args="--title \"$FINDING_ISSUE_TITLE\" --body-file \"$FINDING_ISSUE_BODY_FILE\" --label review-finding --label needs-validation --label \"$FINDING_PRIORITY\" ${MILESTONE_FLAG}")
|
|
1790
1830
|
rm -f "$FINDING_ISSUE_BODY_FILE"
|
|
1791
1831
|
|
|
1792
1832
|
# /issue has no machine-readable return contract (it's a user-facing command, not a work-on
|
|
@@ -1799,15 +1839,16 @@ for _resolve_attempt in 1 2 3; do
|
|
|
1799
1839
|
[ -n "$ISSUE_NUM" ] && break
|
|
1800
1840
|
sleep 2
|
|
1801
1841
|
done
|
|
1842
|
+
fi
|
|
1802
1843
|
```
|
|
1803
1844
|
|
|
1804
|
-
Labels: `review-finding` + `needs-validation` + priority (`priority:P1`
|
|
1845
|
+
Labels: `review-finding` + `needs-validation` + priority. `priority:*` is derived from the finding's `**Severity**` field via `scripts/severity-to-priority.sh` (single documented mapping — see that script's header comment): `CRITICAL` → `priority:P0`, `HIGH` → `priority:P1`, `MEDIUM` → `priority:P2`, `LOW` → `priority:P3`. **Never derive `priority:*` from Confidence** (CONFIRMED/LIKELY/POSSIBLE) — Confidence and Severity are independent axes; conflating them previously mislabeled LOW-severity CONFIRMED findings as `priority:P1`, defeating the P3 batching rule below. <!-- forge#2447 -->
|
|
1805
1846
|
|
|
1806
|
-
**P3 batchable annotation** — <!-- Added: forge#1333 --> When priority resolves to `priority:P3` (
|
|
1847
|
+
**P3 batchable annotation** — <!-- Added: forge#1333 --> When priority resolves to `priority:P3` (i.e. `**Severity**: LOW`), check whether the finding qualifies for batching. If the affected file does NOT touch a security or billing path, substitute `[BATCHABLE_ANNOTATION]` with the `<!-- FORGE:BATCHABLE -->` marker. This signals the orchestrator's Phase 1 batching rule that this finding can be grouped with other P3s in the same domain:
|
|
1807
1848
|
|
|
1808
1849
|
```bash
|
|
1809
1850
|
# Determine batchable eligibility for this finding
|
|
1810
|
-
|
|
1851
|
+
# $FINDING_PRIORITY was already computed above (severity-to-priority.sh) — reuse it, do not re-derive.
|
|
1811
1852
|
BATCHABLE_ANNOTATION=""
|
|
1812
1853
|
|
|
1813
1854
|
if [ "$FINDING_PRIORITY" = "priority:P3" ]; then
|
|
@@ -2089,19 +2130,41 @@ if [ "$PRE_MERGE_HEALTH" = "CONFLICTING" ] || [ "$PRE_MERGE_HEALTH_STATE" = "DIR
|
|
|
2089
2130
|
# STOP — do not attempt gh pr merge on a CONFLICTING/DIRTY PR
|
|
2090
2131
|
else
|
|
2091
2132
|
|
|
2092
|
-
# Previously-escalated re-review guard <!-- Added: forge#1810 -->
|
|
2133
|
+
# Previously-escalated re-review guard <!-- Added: forge#1810; base-scoped: forge#2570 -->
|
|
2093
2134
|
# If the linked issue currently carries needs-human, this PR was escalated at some
|
|
2094
2135
|
# earlier point (VERDICT/purpose-regression/calibration/trust/mergeability) and has
|
|
2095
2136
|
# now been remediated + re-reviewed back to a clean, mergeable APPROVED state above.
|
|
2096
|
-
#
|
|
2097
|
-
# #
|
|
2098
|
-
#
|
|
2099
|
-
#
|
|
2137
|
+
#
|
|
2138
|
+
# forge#2570: the hold is now scoped by the PR's target branch. This PR still transitions
|
|
2139
|
+
# to workflow:awaiting-merge (clearing needs-human) in BOTH cases below — the transition is
|
|
2140
|
+
# unchanged; only who acts next differs:
|
|
2141
|
+
# - `main` / deploy gate (or an unresolved base — fail closed): held for a HUMAN merge
|
|
2142
|
+
# decision, because `staging → main` is the genuine human gate no agent performs.
|
|
2143
|
+
# - any other base (staging, milestone/* — reversible integration branches): NOT held for
|
|
2144
|
+
# a human. It is parked for remediate.md Phase M7's base-scoped auto-land bar (bot APPROVED
|
|
2145
|
+
# + quality-gate pass), which merges it on the SAME condition the normal /work-on fast lane
|
|
2146
|
+
# uses for the same target branch — no manual click. This reconciles the asymmetry where a
|
|
2147
|
+
# bot-only re-review (authorAssociation=NONE) could never satisfy the strict ≥2 verified-
|
|
2148
|
+
# human bar and so stranded staging PRs the fast lane would auto-merge.
|
|
2100
2149
|
PREVIOUSLY_ESCALATED=$(gh issue view {MERGE_ISSUE} {MERGE_GH_FLAG} --json labels \
|
|
2101
2150
|
--jq '[.labels[].name] | any(. == "needs-human")' 2>/dev/null || echo "false")
|
|
2151
|
+
GUARD_BASE=$(gh pr view {PR_NUMBER} {MERGE_GH_FLAG} --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
2102
2152
|
|
|
2103
2153
|
if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
2104
|
-
|
|
2154
|
+
# forge#2625: `jq -r '.baseRefName'` stringifies a JSON null to the literal text "null" (not
|
|
2155
|
+
# an empty string), so the `-n` check alone does not catch it — add an explicit `!= "null"`
|
|
2156
|
+
# check so a literal-string "null" is treated the same as an empty/unresolved base (strict).
|
|
2157
|
+
if [ -n "$GUARD_BASE" ] && [ "$GUARD_BASE" != "null" ] && [ "$GUARD_BASE" != "main" ]; then
|
|
2158
|
+
# forge#2570: non-`main` base — defer to remediate.md M7's auto-land, not a human.
|
|
2159
|
+
# forge#2626: standalone `/review-pr` (no remediate.md M6→M7 chain) would otherwise
|
|
2160
|
+
# strand this PR at `workflow:awaiting-merge` with no actionable guidance — add a
|
|
2161
|
+
# manual-merge fallback so the comment is correct in both the chained and standalone cases.
|
|
2162
|
+
# GOVERNOR-exempt: intentional coordination side-effect (best-effort lease/board/finding post), DRY_RUN-safe — reviewed & accepted for the check-command-side-effects gate. Flagged only by the staging->main full-diff; passes on every feature PR. forge#2627
|
|
2163
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check on non-\`main\` base \`${GUARD_BASE}\`. It is parked at \`workflow:awaiting-merge\` for the remediation auto-land bar (\`remediate.md\` Phase M7, forge#2570) — which auto-merges it on the same fast-lane condition the normal \`/work-on\` path uses for this target branch — NOT for a human merge decision. If no remediation \`M7\` follows (e.g. this review was invoked standalone), merge manually once reviewed: \`gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge\`."
|
|
2164
|
+
else
|
|
2165
|
+
# forge#2570: `main` / deploy gate (or unresolved base — fail closed to the human gate).
|
|
2166
|
+
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "🟠 PR #{PR_NUMBER} was previously escalated (\`needs-human\`) and has now been re-reviewed to \`${VERDICT:-APPROVED}\` with a clean mergeability check. This PR targets the deploy gate (\`${GUARD_BASE:-unresolved base}\`), so it is held at \`workflow:awaiting-merge\` for a human merge decision — \`staging → main\` is the genuine human gate. Merge manually once reviewed: \`gh pr merge {PR_NUMBER} {MERGE_GH_FLAG} --merge\`."
|
|
2167
|
+
fi
|
|
2105
2168
|
RESOLUTION=$(resolve_script 'transition-label')
|
|
2106
2169
|
TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
2107
2170
|
case "$TIER" in
|
|
@@ -2111,7 +2174,9 @@ if [ "$PREVIOUSLY_ESCALATED" = "true" ]; then
|
|
|
2111
2174
|
--remove-label "needs-human,workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true
|
|
2112
2175
|
;;
|
|
2113
2176
|
esac
|
|
2114
|
-
# STOP — do not attempt gh pr merge
|
|
2177
|
+
# STOP — do not attempt gh pr merge here. The merge decision belongs to a human for a
|
|
2178
|
+
# `main`/deploy-gate base, or to remediate.md Phase M7's base-scoped auto-land bar for a
|
|
2179
|
+
# non-`main` base (forge#2570). Either way this guard only parks at workflow:awaiting-merge.
|
|
2115
2180
|
else
|
|
2116
2181
|
# Checkpoint comment on issue
|
|
2117
2182
|
gh issue comment {MERGE_ISSUE} {MERGE_GH_FLAG} --body "Review complete for PR #{PR_NUMBER}. Verdict: ${VERDICT:-APPROVED}. Proceeding to merge."
|
|
@@ -29,7 +29,7 @@ COMPLEXITY_BAND=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
|
29
29
|
COMPLEXITY_BAND="${COMPLEXITY_BAND:-STANDARD}"
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
**If COMPLEXITY_BAND: TRIVIAL** → skip all phases (A0 through A5), post
|
|
32
|
+
**If COMPLEXITY_BAND: TRIVIAL** → skip all phases (A0 through A5), **post the minimal skip marker comment described in "Skip Marker" under Skip Conditions below** (reason: `TRIVIAL complexity band`), then return an empty plan to caller immediately. Do not read any files. This is not an error — trivial single-file changes have no cross-path consistency risk. The skip marker (ending in `<!-- FORGE:ARCHITECT:COMPLETE -->`) is what makes a legitimate skip observable to the headless engine's marker-only gate, so the issue advances to build instead of being stranded at `needs-human`. <!-- Added: forge#679; skip-marker on skip: forge#2689 -->
|
|
33
33
|
|
|
34
34
|
**If COMPLEXITY_BAND: STANDARD or COMPLEX** → proceed to Phase A0 below.
|
|
35
35
|
|
|
@@ -668,14 +668,39 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ARCHITECT -->
|
|
|
668
668
|
|
|
669
669
|
## Skip Conditions
|
|
670
670
|
|
|
671
|
-
Skip
|
|
671
|
+
Skip the planning work (Phases A0–A5) and return an empty plan to caller — **but always post the Skip Marker below first** — if:
|
|
672
672
|
- **COMPLEXITY_BAND: TRIVIAL** — checked via FORGE:FAST_PATH comment at entry (see guard above) <!-- Primary skip path: forge#679 -->
|
|
673
673
|
- Issue creates only **new files** with no callers to find (e.g. a new command file with no existing integration point yet)
|
|
674
674
|
- Issue is a 1-file config or docs edit with no code logic
|
|
675
675
|
- Issue title starts with "docs:" or "chore:"
|
|
676
676
|
- `{AFFECTED_FILES}` is empty
|
|
677
677
|
|
|
678
|
-
When skipped, the builder proceeds with investigation report + context briefing only.
|
|
678
|
+
When skipped, the builder proceeds with investigation report + context briefing only (see Integration Point below — the builder falls back gracefully on an empty plan).
|
|
679
|
+
|
|
680
|
+
### Skip Marker (MANDATORY on every skip — do NOT post nothing) <!-- forge#2689 -->
|
|
681
|
+
|
|
682
|
+
**Do not post nothing.** The headless engine gate (`bin/engine/phases.mjs` → `architect.detectOutcome`) treats a **missing** `FORGE:ARCHITECT:COMPLETE` sentinel as a phase **failure**, retries to `maxAttempts` (3), then strands the issue at `needs-human`. It reads GitHub comment markers only — it cannot see this subcommand's in-process "empty plan" return value. So a skip that posts no marker is indistinguishable from a crashed architect run and deterministically strands **every** `chore:`/`docs:`/trivial issue.
|
|
683
|
+
|
|
684
|
+
On **every** skip branch above, post exactly this minimal comment (substitute the concrete `{SKIP_REASON}`, e.g. `chore:/docs: title`, `TRIVIAL complexity band`, `new files only — no callers`, or `AFFECTED_FILES empty`) **before** returning the empty plan:
|
|
685
|
+
|
|
686
|
+
```bash
|
|
687
|
+
# The skip marker is mandatory and un-gated by design — posting it IS the point
|
|
688
|
+
# (a missing marker strands the issue at needs-human; see above). Same report-only
|
|
689
|
+
# risk class as the unguarded plan post in "Output Format" above, so it carries the
|
|
690
|
+
# same allowlist rather than a DRY_RUN guard.
|
|
691
|
+
SKIP_BODY=$(cat <<SKIP_EOF
|
|
692
|
+
<!-- FORGE:ARCHITECT -->
|
|
693
|
+
## Architecture Plan — Skipped
|
|
694
|
+
|
|
695
|
+
**Skipped**: {SKIP_REASON}. No cross-path consistency risk — the builder proceeds with the investigation report + context briefing.
|
|
696
|
+
|
|
697
|
+
<!-- FORGE:ARCHITECT:COMPLETE -->
|
|
698
|
+
SKIP_EOF
|
|
699
|
+
)
|
|
700
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "$SKIP_BODY" # <!-- allowlist:check-command-side-effects -->
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
The empty-plan-with-marker changes no downstream behavior — the builder already falls back to the investigation report + contract when the plan is empty (see Integration Point below). The marker only makes the legitimate skip **observable** to the marker-only engine gate. This intentionally does **not** weaken crash detection: a genuinely interrupted architect run still posts no marker at all, so `detectOutcome` still fails-and-escalates it correctly.
|
|
679
704
|
|
|
680
705
|
---
|
|
681
706
|
|
|
@@ -255,11 +255,51 @@ REVIEW_BODIES=$(gh pr view {PR_NUMBER} {GH_FLAG} --json reviews,comments \
|
|
|
255
255
|
APPROVED_COUNT=$(echo "$REVIEW_BODIES" | grep -cE 'APPROVED:' 2>/dev/null || true); APPROVED_COUNT=${APPROVED_COUNT:-0}
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
-
**Auto-land bar** (
|
|
258
|
+
**Auto-land bar — base-branch scoped** (forge#2570): the condition the PR must clear to auto-land depends on its target branch. This reconciles the remediation bar with the normal `/work-on` fast-lane merge bar for the *same target branch*, while keeping the strict human-verified bar only where a human is genuinely in the loop (the `staging → main` deploy gate).
|
|
259
|
+
|
|
260
|
+
**Re-derive the base fresh — do NOT reuse `$PR_BASE` for this decision** (forge#2624): `$PR_BASE` was resolved in Phase M0 as `PR_BASE="${PR_BASE:-$(... .baseRefName)}"` — a caller-supplied `--base` wins over the PR's actual live base whenever non-empty. That is fine for M0's own purposes (worktree base, display text), but this is a security-relevant decision point: a wrong/stale `--base staging` on a PR that actually targets `main` must never be allowed to relax the strict deploy-gate bar. Mirror `review-pr.md`'s sibling guard (`GUARD_BASE`, which always re-fetches `baseRefName` fresh at its own decision point) by re-querying the PR's live base here, independent of whatever `$PR_BASE` currently holds:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# forge#2624: re-fetch baseRefName fresh from the PR itself — never trust the
|
|
264
|
+
# M0-resolved $PR_BASE for this decision, since M0 prefers a caller-supplied
|
|
265
|
+
# --base over the live value. This is the one line in this phase that makes
|
|
266
|
+
# a trust/security decision, so it must be immune to a caller-overridable input.
|
|
267
|
+
LIVE_BASE_REF=$(gh pr view {PR_NUMBER} {GH_FLAG} --json baseRefName --jq '.baseRefName' 2>/dev/null || echo "")
|
|
268
|
+
|
|
269
|
+
# forge#2570: `main` (and any deploy-gate base) keeps the strict #1809 Q1 verified-human bar;
|
|
270
|
+
# every other base (staging, milestone/* — the reversible integration branches) reconciles to
|
|
271
|
+
# the fast-lane bar. Key on "is the deploy gate", NOT the literal string "staging", so milestone
|
|
272
|
+
# branches reconcile too. Fail closed: only a KNOWN, non-empty, non-`main` LIVE base reconciles
|
|
273
|
+
# to the fast lane; an empty/unresolved fetch is treated as the deploy gate (strict) so a base-
|
|
274
|
+
# resolution failure (or a caller passing a stale/incorrect --base) can never accidentally
|
|
275
|
+
# relax the bar.
|
|
276
|
+
# forge#2625: `jq -r '.baseRefName'` stringifies a JSON null to the literal text "null" (not an
|
|
277
|
+
# empty string), so the `-n` check alone does not catch it — add an explicit `!= "null"` check
|
|
278
|
+
# so a literal-string "null" is treated the same as an empty/unresolved base (strict).
|
|
279
|
+
if [ -n "$LIVE_BASE_REF" ] && [ "$LIVE_BASE_REF" != "null" ] && [ "$LIVE_BASE_REF" != "main" ]; then IS_DEPLOY_GATE=false; else IS_DEPLOY_GATE=true; fi
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Non-`main` base (`IS_DEPLOY_GATE=false` — staging / milestone)** — reconcile to the fast-lane bar. `review-pr.md`'s Phase 8 guard only parks a PR at `workflow:awaiting-merge` after a clean, mergeable `APPROVED` re-review (the same bot-`APPROVED` signal the normal fast lane auto-merges on), so the only additional condition is this remediation's own quality gate:
|
|
283
|
+
1. `GATE_PASSED = true` from this remediation's Phase M3 quality-gate loop.
|
|
284
|
+
|
|
285
|
+
The strict `APPROVED_COUNT >= 2` verified-human requirement does NOT apply here: it is structurally unsatisfiable for bot-only pipeline review (bot reviews are `authorAssociation=NONE`), and `staging` is reversible — the real human gate is `staging → main`, which no agent performs. This makes the remediation bar identical to the normal fast-lane bar for the same target branch (the issue's core ask). The `authorAssociation` trust filter above is **unchanged** — the relaxation is scoped by *target branch only*, never by *who* may approve (forge#1976/#2519).
|
|
286
|
+
|
|
287
|
+
**`main` base (`IS_DEPLOY_GATE=true` — deploy gate)** — keep the strict #1809 Q1 bar, BOTH conditions required:
|
|
259
288
|
1. `APPROVED_COUNT >= 2` — at least two distinct adversarial `APPROVED:` review comments from repo collaborators (`OWNER`/`MEMBER`/`COLLABORATOR` authorAssociation only — see trust filter above; same counting convention as `work-on.md` Phase 7A).
|
|
260
289
|
2. `GATE_PASSED = true` from this remediation's own Phase M3 quality-gate loop.
|
|
261
290
|
|
|
262
|
-
**
|
|
291
|
+
**Evaluate the base-scoped bar**:
|
|
292
|
+
```bash
|
|
293
|
+
if [ "$IS_DEPLOY_GATE" = "true" ]; then
|
|
294
|
+
# main / deploy gate — strict verified-human bar
|
|
295
|
+
{ [ "${APPROVED_COUNT:-0}" -ge 2 ] && [ "${GATE_PASSED:-false}" = "true" ]; } && BAR_MET=true || BAR_MET=false
|
|
296
|
+
else
|
|
297
|
+
# staging / milestone — fast-lane bar (bot APPROVED already implied by workflow:awaiting-merge)
|
|
298
|
+
[ "${GATE_PASSED:-false}" = "true" ] && BAR_MET=true || BAR_MET=false
|
|
299
|
+
fi
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**If the bar is met** (`BAR_MET=true`):
|
|
263
303
|
```bash
|
|
264
304
|
gh pr merge {PR_NUMBER} {GH_FLAG} --merge
|
|
265
305
|
MERGE_STATE=$(gh pr view {PR_NUMBER} {GH_FLAG} --json state --jq '.state')
|
|
@@ -281,7 +321,7 @@ else
|
|
|
281
321
|
fi
|
|
282
322
|
```
|
|
283
323
|
|
|
284
|
-
**If the bar is NOT met
|
|
324
|
+
**If the bar is NOT met** (`BAR_MET=false`): leave the issue at `workflow:awaiting-merge` exactly as `review-pr.md`'s guard set it — do NOT attempt a merge. `RE_GATE_OUTCOME="HELD-AWAITING-MERGE"`. Fail-safe direction: any doubt about the bar (including an unresolved or unexpected `LIVE_BASE_REF`, which leaves `IS_DEPLOY_GATE=true`-equivalent strict handling) defaults to holding, matching `review-pr.md`'s own existing default for every other caller.
|
|
285
325
|
|
|
286
326
|
---
|
|
287
327
|
|
|
@@ -291,7 +331,16 @@ Post the completion body to **both** `{PR_NUMBER}` and `{ISSUE_NUMBER}` — this
|
|
|
291
331
|
|
|
292
332
|
```bash
|
|
293
333
|
case "$RE_GATE_OUTCOME" in
|
|
294
|
-
AUTO-LANDED)
|
|
334
|
+
AUTO-LANDED)
|
|
335
|
+
# forge#2570: the bar that was met differs by target branch. Non-`main` (staging/milestone)
|
|
336
|
+
# lands on the fast-lane bar (clean re-review APPROVED + quality-gate pass), identical to the
|
|
337
|
+
# normal /work-on path; `main`/deploy-gate lands only on the strict ≥2 verified-human bar.
|
|
338
|
+
if [ "${IS_DEPLOY_GATE:-true}" = "false" ]; then
|
|
339
|
+
AUTO_LAND_BAR_TEXT="MET — fast-lane bar for non-\`main\` base (clean re-review APPROVED + quality gate pass), matching the normal /work-on merge bar for the same target branch"
|
|
340
|
+
else
|
|
341
|
+
AUTO_LAND_BAR_TEXT="MET (${APPROVED_COUNT:-0} APPROVED: reviews + quality gate pass)"
|
|
342
|
+
fi
|
|
343
|
+
OUTCOME_DETAIL="to {PR_BASE}" ;;
|
|
295
344
|
HELD-AWAITING-MERGE) AUTO_LAND_BAR_TEXT="NOT MET (${APPROVED_COUNT:-0} APPROVED: reviews)"; OUTCOME_DETAIL="at workflow:awaiting-merge" ;;
|
|
296
345
|
RE-ESCALATED) AUTO_LAND_BAR_TEXT="N/A — re-escalated before the bar was evaluated"; OUTCOME_DETAIL="at needs-human" ;;
|
|
297
346
|
UNFIXABLE) AUTO_LAND_BAR_TEXT="N/A — unfixable (see Phase M1 classification)"; OUTCOME_DETAIL="at needs-human" ;;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forgedock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Autonomous dev pipeline for Claude Code. GitHub as institutional memory — every issue, PR, and annotation survives context resets and makes the next agent smarter.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"forgedock": "bin/forgedock.mjs"
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Checks:
|
|
7
7
|
* - Completion sentinel presence (§3.2) — for types that define one
|
|
8
8
|
* - Required field presence (§4.1 per-type field tables)
|
|
9
|
-
* - Required field enum values (Verdict, Confidence, Severity, Task Type)
|
|
9
|
+
* - Required field enum values (Verdict, Confidence, Severity, Task Type, Re-gate outcome)
|
|
10
10
|
* - Inline-value non-empty (§3.4)
|
|
11
11
|
* - CARD inline-value decodability + sha8 integrity (§4.2 — see card.js)
|
|
12
12
|
* - Partial sentinel marking (§3.3) — generates a warning, not an error
|
|
@@ -129,5 +129,28 @@ export function validate(annotation) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// REMEDIATION-specific enum validation (forge#2450): the Re-gate outcome field is
|
|
133
|
+
// optional at the required-field-presence layer (it is absent until
|
|
134
|
+
// commands/work-on/remediate.md's Phase M8 posts it), but when present its value
|
|
135
|
+
// must come from the single-sourced registry list — not a second hardcoded copy.
|
|
136
|
+
//
|
|
137
|
+
// Phase M8 posts this field as "**Re-gate outcome**: ${RE_GATE_OUTCOME} ${OUTCOME_DETAIL}"
|
|
138
|
+
// (see commands/work-on/remediate.md), so the parsed field value carries trailing free-text
|
|
139
|
+
// detail (e.g. "AUTO-LANDED to staging"), not just the bare outcome token. Only the leading
|
|
140
|
+
// token is the actual outcome — extract it the same way bin/engine/phases.mjs's own
|
|
141
|
+
// `/\*\*Re-gate outcome\*\*:\s*([A-Z-]+)/` regex does, so real production annotations with
|
|
142
|
+
// trailing detail text are not falsely rejected.
|
|
143
|
+
if (type === 'REMEDIATION') {
|
|
144
|
+
const reGateOutcome = fields['Re-gate outcome'];
|
|
145
|
+
if (reGateOutcome) {
|
|
146
|
+
const outcomeToken = reGateOutcome.match(/^([A-Z-]+)/)?.[1] ?? reGateOutcome;
|
|
147
|
+
if (!typeDef.reGateOutcomeValues.includes(outcomeToken)) {
|
|
148
|
+
errors.push(
|
|
149
|
+
`Invalid Re-gate outcome "${outcomeToken}" — must be one of: ${typeDef.reGateOutcomeValues.join(', ')}`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
132
155
|
return { valid: errors.length === 0, errors, warnings };
|
|
133
156
|
}
|
|
@@ -48,7 +48,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, rea
|
|
|
48
48
|
import { execSync, spawnSync } from 'child_process';
|
|
49
49
|
import { homedir } from 'os';
|
|
50
50
|
import { dirname, join, resolve } from 'path';
|
|
51
|
-
import { fileURLToPath } from 'url';
|
|
51
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
52
52
|
|
|
53
53
|
const __filename = fileURLToPath(import.meta.url);
|
|
54
54
|
const __dirname = dirname(__filename);
|
|
@@ -309,7 +309,11 @@ async function getParser() {
|
|
|
309
309
|
const parsePath = join(repoRoot, 'packages', 'protocol', 'src', 'parse.js');
|
|
310
310
|
|
|
311
311
|
try {
|
|
312
|
-
|
|
312
|
+
// On win32 a bare absolute path (C:\...) makes Node parse the drive letter as a
|
|
313
|
+
// URL scheme (ERR_UNSUPPORTED_ESM_URL_SCHEME). Convert to a file:// URL first —
|
|
314
|
+
// valid on POSIX too, and matches the import(pathToFileURL(...).href) idiom used
|
|
315
|
+
// at check-admission-parity.mjs / validate-annotation-node.mjs / check-phase-registry-drift.mjs.
|
|
316
|
+
const mod = await import(pathToFileURL(parsePath).href);
|
|
313
317
|
_parseAnnotation = mod.parse;
|
|
314
318
|
return _parseAnnotation;
|
|
315
319
|
} catch (e) {
|
|
@@ -69,6 +69,28 @@ async function main() {
|
|
|
69
69
|
process.exit(1);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// Verify every id in PHASE_IDS has a corresponding key in PHASE_MARKERS.
|
|
73
|
+
// The empty-marker check below iterates Object.entries(PHASE_MARKERS), which
|
|
74
|
+
// only visits keys that actually exist on the object — a phase id that is
|
|
75
|
+
// entirely absent from PHASE_MARKERS (as opposed to present-but-empty) never
|
|
76
|
+
// appears in that iteration, so it would pass silently here and only surface
|
|
77
|
+
// later as a runtime TypeError in bin/engine/phases.mjs (e.g.
|
|
78
|
+
// `PHASE_MARKERS.someId.completionMarker` on an undefined `someId` entry).
|
|
79
|
+
// Catch that gap explicitly before the empty-marker check below.
|
|
80
|
+
const phaseMarkers = registryModule.PHASE_MARKERS || {};
|
|
81
|
+
const missingKeys = registryIds.filter(
|
|
82
|
+
(id) => !Object.prototype.hasOwnProperty.call(phaseMarkers, id)
|
|
83
|
+
);
|
|
84
|
+
if (missingKeys.length > 0) {
|
|
85
|
+
console.error(
|
|
86
|
+
`ERROR: PHASE_MARKERS is missing an entry entirely for: ${missingKeys.join(", ")}`
|
|
87
|
+
);
|
|
88
|
+
console.error(
|
|
89
|
+
" Add a corresponding key to packages/protocol/src/phases.js's PHASE_MARKERS for each id above."
|
|
90
|
+
);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
72
94
|
// Also verify every registry marker entry that has a `completionMarker` or
|
|
73
95
|
// `completionLabel` field is a non-empty string — catches an accidental
|
|
74
96
|
// undefined/null slipping in (e.g. a typo'd RESERVED_TYPES key whose
|
|
@@ -76,7 +98,7 @@ async function main() {
|
|
|
76
98
|
// has(blob, undefined) → substring "undefined" never matches, masking a
|
|
77
99
|
// real bug as "phase never completes".
|
|
78
100
|
const emptyMarkers = [];
|
|
79
|
-
for (const [phaseId, entry] of Object.entries(
|
|
101
|
+
for (const [phaseId, entry] of Object.entries(phaseMarkers)) {
|
|
80
102
|
const marker = entry.completionMarker ?? entry.completionLabel;
|
|
81
103
|
if (typeof marker !== "string" || marker.length === 0) {
|
|
82
104
|
emptyMarkers.push(phaseId);
|
package/scripts/classify-lane.sh
CHANGED
|
@@ -70,10 +70,15 @@ if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then
|
|
|
70
70
|
exit 1
|
|
71
71
|
fi
|
|
72
72
|
|
|
73
|
-
# Fetch milestone title
|
|
74
|
-
# gh issue view exits non-zero if
|
|
73
|
+
# Fetch milestone title (and labels, for the review-finding staging-default
|
|
74
|
+
# note below) from GitHub in a single call — gh issue view exits non-zero if
|
|
75
|
+
# the issue does not exist or auth fails. Labels are fetched here rather
|
|
76
|
+
# than via a second `gh issue view` call so this stays one round trip, same
|
|
77
|
+
# failure surface as before this field was added. <!-- Added: forge#2443 -->
|
|
75
78
|
GH_STDERR_TMP=$(mktemp)
|
|
76
|
-
|
|
79
|
+
chmod 600 "$GH_STDERR_TMP"
|
|
80
|
+
trap 'rm -f "$GH_STDERR_TMP"' EXIT
|
|
81
|
+
ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" "${GH_REPO_ARGS[@]}" --json milestone,labels 2>"$GH_STDERR_TMP") || {
|
|
77
82
|
echo "ERROR: failed to fetch issue #$ISSUE_NUMBER — check issue number and repo flag" >&2
|
|
78
83
|
cat "$GH_STDERR_TMP" >&2
|
|
79
84
|
rm -f "$GH_STDERR_TMP"
|
|
@@ -81,6 +86,9 @@ MILESTONE_TITLE=$(gh issue view "$ISSUE_NUMBER" "${GH_REPO_ARGS[@]}" --json mile
|
|
|
81
86
|
}
|
|
82
87
|
rm -f "$GH_STDERR_TMP"
|
|
83
88
|
|
|
89
|
+
MILESTONE_TITLE=$(echo "$ISSUE_JSON" | jq -r '.milestone.title // empty')
|
|
90
|
+
HAS_REVIEW_FINDING_LABEL=$(echo "$ISSUE_JSON" | jq -r '[.labels[]?.name] | any(. == "review-finding")')
|
|
91
|
+
|
|
84
92
|
# Resolve staging branch name from forge.yaml (branches.staging), defaulting to "staging".
|
|
85
93
|
# Uses yq if available; falls back gracefully when forge.yaml is absent or yq is not installed.
|
|
86
94
|
STAGING_BRANCH=$(yq '.branches.staging // "staging"' forge.yaml 2>/dev/null || echo 'staging')
|
|
@@ -104,6 +112,24 @@ if [ -z "$MILESTONE_TITLE" ]; then
|
|
|
104
112
|
echo " Run: git push origin HEAD:$STAGING_BRANCH (from the default branch)" >&2
|
|
105
113
|
exit 1
|
|
106
114
|
fi
|
|
115
|
+
# Defense-in-depth visibility (forge#2443): a review-finding issue with no
|
|
116
|
+
# milestone defaults to the staging fast lane here — which is correct ONLY
|
|
117
|
+
# when the finding's subject code actually lives on staging. For a finding
|
|
118
|
+
# spawned from a milestone-lane PR whose body is missing **Code branch**
|
|
119
|
+
# and whose milestone wasn't propagated, this default silently routes the
|
|
120
|
+
# fix to the one branch where the code is guaranteed absent, causing
|
|
121
|
+
# /work-on's investigation phase to close it as invalid. This script has
|
|
122
|
+
# no way to know whether that mismatch applies to THIS issue (it only
|
|
123
|
+
# knows the issue has no milestone) — the note below is diagnostic only,
|
|
124
|
+
# non-fatal, so callers relying on stdout are unaffected.
|
|
125
|
+
# WIRE:PROVEN — manual: ran classify-lane.sh against a fake `gh issue view`
|
|
126
|
+
# returning labels:[{"name":"review-finding"}] and milestone:null; confirmed
|
|
127
|
+
# the NOTE lines below print to stderr while stdout stays unaffected.
|
|
128
|
+
if [ "$HAS_REVIEW_FINDING_LABEL" = "true" ]; then
|
|
129
|
+
echo "NOTE: issue #$ISSUE_NUMBER is a review-finding with no milestone — defaulting to staging fast lane ('$STAGING_BRANCH')." >&2
|
|
130
|
+
echo " If this finding's subject code only exists on a milestone branch, this default is WRONG." >&2
|
|
131
|
+
echo " See commands/orchestrate/phase-4-execution.md Step 4C for the repair/loud-failure guard." >&2
|
|
132
|
+
fi
|
|
107
133
|
if [ "$JSON_OUTPUT" = "true" ]; then
|
|
108
134
|
printf '{"lane":"%s","branch":"%s","source":"fast-lane","milestone":null}\n' \
|
|
109
135
|
"$STAGING_BRANCH" "$STAGING_BRANCH"
|