yadflow 2.15.0 → 2.16.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/CHANGELOG.md CHANGED
@@ -1,9 +1,9 @@
1
- # [2.15.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.14.0...v2.15.0) (2026-06-24)
1
+ # [2.16.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.15.0...v2.16.0) (2026-06-25)
2
2
 
3
3
 
4
4
  ### Features
5
5
 
6
- * merge-driven review gate (Path B) CI never pushes the review branch ([#78](https://github.com/abdelrahmannasr/yadflow/issues/78)) ([d4d983a](https://github.com/abdelrahmannasr/yadflow/commit/d4d983ab4efddb4e6ec259bb940b393e9237f9cf)), closes [#76](https://github.com/abdelrahmannasr/yadflow/issues/76)
6
+ * make hub pr-title/pr-template gates branch-aware so tooling PRs pass ([#79](https://github.com/abdelrahmannasr/yadflow/issues/79)) ([68050e0](https://github.com/abdelrahmannasr/yadflow/commit/68050e000010b4d98f304a7e5e1f6a39bc0c229c))
7
7
 
8
8
  # [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo). A BMAD module + 30 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
@@ -121,8 +121,17 @@ from the event payload):
121
121
  - `--profile code` (default) → a Conventional-Commits subject `<type>: <description>`, no trailing
122
122
  period (`config.yaml build.pr_title_style: same_as_commit_subject` — one task = one PR, the title is
123
123
  the squash-merge subject).
124
- - `--profile hub` → a front-half artifact-review title `review: <artifact> (EP-<slug>)`, the shape
125
- `yad gate open` creates.
124
+ - `--profile hub` → splits by the PR/MR **head branch** (passed via `--head`, injected by CI):
125
+ - `review/EP-*` head (or no `--head` — stays strict) → a front-half artifact-review title
126
+ `review: <artifact> (EP-<slug>)`, the shape `yad gate open` creates.
127
+ - any other head → a tooling/code change to the hub itself, so it follows the `code` convention (a
128
+ Conventional-Commits subject). This is what lets a PR that changes the hub's own workflows/checks
129
+ pass — it has no EP artifact to review.
130
+ - **Anti-bypass guard.** The branch name alone is not trusted: a non-review head that actually
131
+ changes front-half artifacts (any path under `epics/**`) **FAILS** — those changes must go through
132
+ a `review/EP-*` PR and the artifact-review workflow. CI passes the PR's changed paths via
133
+ `--changed <file>` (computed from the diff against the base ref); without that list (a direct
134
+ by-hand caller) the guard is inert and the branch split alone applies.
126
135
 
127
136
  ## 7. pr-template (`templates/checks/pr-template.sh`)
128
137
 
@@ -131,8 +140,14 @@ catches a free-form description that bypassed it:
131
140
 
132
141
  - `--profile code` (default) → requires `## Summary`, `## Impact & Risk`, `## Checklist`, and a filled
133
142
  `Risk level:` (`low|medium|high`).
134
- - `--profile hub` → requires `## Artifact under review`, `## Impact & Risk (front-half)`, `## Checklist`,
135
- and a `Risk tags:` line.
143
+ - `--profile hub` → splits by the PR/MR **head branch** (passed via `--head`, injected by CI):
144
+ - `review/EP-*` head (or no `--head`) requires the artifact-review template: `## Artifact under
145
+ review`, `## Impact & Risk (front-half)`, `## Checklist`, and a `Risk tags:` line.
146
+ - any other head → a hub tooling PR, so it requires the `code` task template (`## Summary`,
147
+ `## Impact & Risk`, `## Checklist`, filled `Risk level:`).
148
+ - **Anti-bypass guard** (same as pr-title): a non-review head that changes front-half artifacts
149
+ (`epics/**`, detected from the CI-supplied `--changed <file>` list) **FAILS** — artifact changes
150
+ must go through a `review/EP-*` PR.
136
151
 
137
152
  ## CI wiring (both platforms)
138
153
 
@@ -202,9 +217,12 @@ its one include line) whenever `.sdlc/hub.json` has a platform with the bridge e
202
217
  front-half review PRs are held to the same rule as code-repo PRs: signed, known authors only.
203
218
 
204
219
  The hub **also** runs the three pattern gates (`commit-message`, `pr-title`, `pr-template`) with
205
- `--profile hub`, so the front-half review PRs follow the hub conventions Conventional-Commits commit
206
- subjects, a `review: <artifact> (EP-<slug>)` title, and a body that uses the hub artifact-review
207
- template. `yad check --fix` installs the same `checks/*.sh` scripts plus a standalone hub workflow
220
+ `--profile hub`. The pattern gates split by the PR/MR **head branch** (passed via `--head`): a
221
+ `review/EP-*` head is a front-half review PR Conventional-Commits commit subjects, a
222
+ `review: <artifact> (EP-<slug>)` title, and the hub artifact-review template body; **any other head is
223
+ a tooling/code change to the hub itself** and follows the `code` convention (a Conventional-Commits
224
+ title + the code task template), so a PR that changes the hub's own workflows/checks can pass.
225
+ `yad check --fix` installs the same `checks/*.sh` scripts plus a standalone hub workflow
208
226
  (`templates/github/yad-hub-checks.yml` → `.github/workflows/yad-hub-checks.yml`, or the GitLab fragment
209
227
  `templates/gitlab/yad-hub-checks.gitlab-ci.yml` → `.gitlab/ci/yad-hub-checks.yml` + its one include
210
228
  line). Code repos run the same three with `--profile code` inside the main `yad-checks` workflow.
@@ -1,8 +1,11 @@
1
1
  # yad-managed: yad-checks
2
- # Pattern gates for the PRODUCT HUB: every PR (including the front-half review/EP-* PRs) must follow
3
- # the hub conventions — Conventional-Commits commit subjects, a `review: <artifact> (EP-<slug>)` PR
4
- # title, and a PR body that uses the hub artifact-review template. They run with `--profile hub`.
5
- # Standalone workflow so it never collides with the code-repo yad-checks workflow.
2
+ # Pattern gates for the PRODUCT HUB. They run with `--profile hub` and split by head branch:
3
+ # review/EP-* PRs are front-half artifact-review vehicles a `review: <artifact> (EP-<slug>)` title
4
+ # and the hub artifact-review template body.
5
+ # every other PR is a tooling/code change to the hub itself — a Conventional-Commits title and the
6
+ # code task template body (same convention the code repos use).
7
+ # The head ref is passed via --head so the gate can tell the two apart. Commit subjects are always
8
+ # Conventional-Commits. Standalone workflow so it never collides with the code-repo yad-checks workflow.
6
9
  name: yad-hub-checks
7
10
  on:
8
11
  pull_request:
@@ -17,23 +20,34 @@ jobs:
17
20
  - run: bash checks/commit-message.sh --profile hub "origin/${{ github.base_ref }}"
18
21
 
19
22
  # Pass the title via env (never interpolate untrusted ${{ }} into a run line — injection-safe).
23
+ # fetch-depth: 0 + the base ref give us the PR's changed paths, so the gate can reject an artifact
24
+ # change (epics/**) riding a non-review head past the front-half review with a plain code title.
20
25
  pr-title:
21
26
  runs-on: ubuntu-latest
22
27
  env:
23
28
  PR_TITLE: ${{ github.event.pull_request.title }}
29
+ PR_HEAD: ${{ github.event.pull_request.head.ref }}
30
+ BASE_REF: ${{ github.base_ref }}
24
31
  steps:
25
32
  - uses: actions/checkout@v4
26
- - run: bash checks/pr-title.sh --profile hub "$PR_TITLE"
33
+ with: { fetch-depth: 0 }
34
+ - run: |
35
+ changed="$(mktemp)"; git diff --name-only "origin/${BASE_REF}...HEAD" > "$changed"
36
+ bash checks/pr-title.sh --profile hub --head "$PR_HEAD" --changed "$changed" "$PR_TITLE"
27
37
 
28
38
  pr-template:
29
39
  runs-on: ubuntu-latest
30
40
  env:
31
41
  PR_BODY: ${{ github.event.pull_request.body }}
42
+ PR_HEAD: ${{ github.event.pull_request.head.ref }}
43
+ BASE_REF: ${{ github.base_ref }}
32
44
  steps:
33
45
  - uses: actions/checkout@v4
46
+ with: { fetch-depth: 0 }
34
47
  - run: |
48
+ changed="$(mktemp)"; git diff --name-only "origin/${BASE_REF}...HEAD" > "$changed"
35
49
  body="$(mktemp)"; printf '%s' "$PR_BODY" > "$body"
36
- bash checks/pr-template.sh --profile hub "$body"
50
+ bash checks/pr-template.sh --profile hub --head "$PR_HEAD" --changed "$changed" "$body"
37
51
 
38
52
  # The gate ledger is CI-owned: reject non-bot commits to .sdlc/*.json or reviews/*.md.
39
53
  ledger-guard:
@@ -3,9 +3,11 @@
3
3
  # .gitlab-ci.yml via:
4
4
  # include:
5
5
  # - local: '.gitlab/ci/yad-hub-checks.yml'
6
- # Every MR (including the front-half review/EP-* MRs) must follow the hub conventions — a
7
- # Conventional-Commits commit subject, a `review: <artifact> (EP-<slug>)` MR title, and an MR body
8
- # that uses the hub artifact-review template. They run with `--profile hub`.
6
+ # MRs run with `--profile hub` and split by source branch: review/EP-* MRs are front-half artifact-
7
+ # review vehicles (a `review: <artifact> (EP-<slug>)` title + the hub artifact-review template body);
8
+ # every other MR is a tooling/code change to the hub itself and follows the code convention (a
9
+ # Conventional-Commits title + the code task template body). The source branch is passed via --head so
10
+ # the gate can tell the two apart. Commit subjects are always Conventional-Commits.
9
11
  # Job names are yad-hub-prefixed so they coexist with any code-repo fragment in one pipeline; jobs use
10
12
  # `needs: []` and no `stage:` so a foreign `stages:` list can neither break nor reorder them.
11
13
  default:
@@ -25,18 +27,22 @@ yad-hub-commit-message:
25
27
  script:
26
28
  - bash checks/commit-message.sh --profile hub "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
27
29
 
30
+ # GIT_DEPTH: 0 (above) gives the changed paths, so the gate can reject an artifact change (epics/**)
31
+ # riding a non-review source branch past the front-half review with a plain code title/template.
28
32
  yad-hub-pr-title:
29
33
  extends: .yad_hub_mr_only
30
34
  needs: []
31
35
  script:
32
- - bash checks/pr-title.sh --profile hub "$CI_MERGE_REQUEST_TITLE"
36
+ - changed="$(mktemp)"; git diff --name-only "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD" > "$changed"
37
+ - bash checks/pr-title.sh --profile hub --head "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --changed "$changed" "$CI_MERGE_REQUEST_TITLE"
33
38
 
34
39
  yad-hub-pr-template:
35
40
  extends: .yad_hub_mr_only
36
41
  needs: []
37
42
  script:
43
+ - changed="$(mktemp)"; git diff --name-only "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD" > "$changed"
38
44
  - body="$(mktemp)"; printf '%s' "$CI_MERGE_REQUEST_DESCRIPTION" > "$body"
39
- - bash checks/pr-template.sh --profile hub "$body"
45
+ - bash checks/pr-template.sh --profile hub --head "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --changed "$changed" "$body"
40
46
 
41
47
  # The gate ledger is CI-owned: reject non-bot commits to .sdlc/*.json or reviews/*.md.
42
48
  yad-hub-ledger-guard:
@@ -7,21 +7,38 @@
7
7
  # requires `## Summary`, `## Impact & Risk`, `## Checklist`, and a filled `Risk level:` (low|medium|high).
8
8
  # --profile hub — the front-half artifact-review template (templates/hub/<platform>/):
9
9
  # requires `## Artifact under review`, `## Impact & Risk (front-half)`, `## Checklist`, and a `Risk tags:` line.
10
+ # BUT only for review/EP-* head branches. Every other hub PR is a tooling/code change to the hub
11
+ # itself and uses the code task template instead; pass the head ref via --head so the gate knows
12
+ # which template to require. With no --head, the hub profile stays strict (artifact-review template).
13
+ # Branch name is not enough on its own: a non-review head that actually changes front-half
14
+ # artifacts (epics/**) would otherwise slip past the review workflow with only the code template.
15
+ # Pass the PR's changed paths via --changed <file> (one path per line); when they touch epics/**
16
+ # on a non-review head the gate FAILS — artifact changes must go through a review/EP-* PR.
10
17
  # The body is passed as a FILE path (single positional arg); CI writes the event body to a temp file
11
18
  # (GitHub: github.event.pull_request.body; GitLab: $CI_MERGE_REQUEST_DESCRIPTION).
12
19
  set -euo pipefail
13
20
 
14
21
  PROFILE=code
22
+ HEADREF=""
23
+ CHANGED=""
15
24
  ARGS=()
16
25
  while [ $# -gt 0 ]; do
17
26
  case "$1" in
18
27
  --profile) PROFILE="${2:-code}"; shift 2 ;;
19
28
  --profile=*) PROFILE="${1#*=}"; shift ;;
29
+ --head) HEADREF="${2:-}"; shift 2 ;;
30
+ --head=*) HEADREF="${1#*=}"; shift ;;
31
+ --changed) CHANGED="${2:-}"; shift 2 ;;
32
+ --changed=*) CHANGED="${1#*=}"; shift ;;
20
33
  *) ARGS+=("$1"); shift ;;
21
34
  esac
22
35
  done
23
36
  case "$PROFILE" in code|hub) ;; *) echo "FAIL [pr-template]: unknown --profile '$PROFILE' (code|hub)."; exit 1 ;; esac
24
37
 
38
+ # True when the PR changes a front-half artifact (anything under epics/**). Reads the --changed list
39
+ # of paths CI computed from the PR diff; with no list (direct caller / test) it reports false.
40
+ artifact_changed() { [ -n "$CHANGED" ] && [ -f "$CHANGED" ] && grep -qE '^epics/' "$CHANGED"; }
41
+
25
42
  BODY="${ARGS[0]:-}"
26
43
  if [ -z "$BODY" ] || [ ! -f "$BODY" ]; then
27
44
  echo "FAIL [pr-template]: body file not found — pass the PR/MR description as a file path."
@@ -36,19 +53,14 @@ require_heading() {
36
53
  fi
37
54
  }
38
55
 
39
- if [ "$PROFILE" = hub ]; then
40
- require_heading '## Artifact under review' '## Artifact under review'
41
- require_heading '## Impact & Risk \(front-half\)' '## Impact & Risk (front-half)'
42
- require_heading '## Checklist' '## Checklist'
43
- if ! grep -qiE '(\*\*)?Risk tags:' "$BODY"; then
44
- echo "FAIL [pr-template]: missing 'Risk tags:' line (front-half Impact & Risk)."
45
- rc=1
46
- fi
47
- else
56
+ # The code task template: `## Summary`, `## Impact & Risk`, `## Checklist`, and a filled `Risk level:`.
57
+ # Used by the code profile and by hub tooling PRs (any head branch that is not review/EP-*).
58
+ check_code_body() {
48
59
  require_heading '## Summary' '## Summary'
49
60
  require_heading '## Impact & Risk' '## Impact & Risk'
50
61
  require_heading '## Checklist' '## Checklist'
51
62
  # Risk level must be present AND filled with a real value (not the <placeholder>).
63
+ local rl
52
64
  rl="$(grep -iE '(\*\*)?Risk level:' "$BODY" | head -1 \
53
65
  | sed -E 's/<!--.*$//; s/^[^:]*://; s/[*`]//g; s/^[[:space:]]*//; s/[[:space:]]*$//' || true)"
54
66
  rl="$(printf '%s' "$rl" | tr 'A-Z' 'a-z' | grep -oE 'low|medium|high' | head -1 || true)"
@@ -56,7 +68,38 @@ else
56
68
  echo "FAIL [pr-template]: 'Risk level:' missing or not set to low|medium|high."
57
69
  rc=1
58
70
  fi
71
+ }
72
+
73
+ # The front-half artifact-review template.
74
+ check_hub_body() {
75
+ require_heading '## Artifact under review' '## Artifact under review'
76
+ require_heading '## Impact & Risk \(front-half\)' '## Impact & Risk (front-half)'
77
+ require_heading '## Checklist' '## Checklist'
78
+ if ! grep -qiE '(\*\*)?Risk tags:' "$BODY"; then
79
+ echo "FAIL [pr-template]: missing 'Risk tags:' line (front-half Impact & Risk)."
80
+ rc=1
81
+ fi
82
+ }
83
+
84
+ KIND="$PROFILE"
85
+ if [ "$PROFILE" = hub ]; then
86
+ case "$HEADREF" in
87
+ review/EP-*|"") check_hub_body ;; # artifact-review PR (or unknown head — stay strict)
88
+ *)
89
+ # tooling/code change to the hub itself — UNLESS it changes front-half artifacts (epics/**),
90
+ # which must go through a review/EP-* PR. Without this guard a non-review head could carry an
91
+ # artifact change past the front-half review with only the code template.
92
+ if artifact_changed; then
93
+ echo "FAIL [pr-template]: head '${HEADREF}' changes front-half artifacts (epics/**) but is not a review/EP-* branch — artifact changes must go through a review PR."
94
+ rc=1
95
+ else
96
+ check_code_body; KIND="hub-tooling"
97
+ fi
98
+ ;;
99
+ esac
100
+ else
101
+ check_code_body
59
102
  fi
60
103
 
61
- [ "$rc" = 0 ] && echo "PASS [pr-template]: body uses the ${PROFILE} template (required sections present)."
104
+ [ "$rc" = 0 ] && echo "PASS [pr-template]: body uses the ${KIND} template (required sections present)."
62
105
  exit "$rc"
@@ -5,22 +5,39 @@
5
5
  # period (config.yaml build.pr_title_style: same_as_commit_subject; one task = one PR, the title is
6
6
  # the squash-merge subject). Keep <type> in sync with cli/manifest.mjs COMMIT_TYPES.
7
7
  # --profile hub — a front-half artifact-review title "review: <artifact> (EP-<slug>)", the shape
8
- # `yad gate open` creates (cli/gate.mjs).
8
+ # `yad gate open` creates (cli/gate.mjs) — BUT only for review/EP-* head branches. Every other
9
+ # hub PR is a tooling/code change to the hub itself and follows the code convention; pass the
10
+ # head ref via --head so the gate can tell the two apart (a tooling PR has no EP artifact to
11
+ # review). With no --head, the hub profile stays strict (requires the review shape).
12
+ # Branch name is not enough on its own: a non-review head that actually changes front-half
13
+ # artifacts (epics/**) would otherwise slip past the review workflow with a plain code title.
14
+ # Pass the PR's changed paths via --changed <file> (one path per line); when they touch epics/**
15
+ # on a non-review head the gate FAILS — artifact changes must go through a review/EP-* PR.
9
16
  # The title is passed as the (single) positional arg; CI injects it from the event payload
10
17
  # (GitHub: github.event.pull_request.title; GitLab: $CI_MERGE_REQUEST_TITLE).
11
18
  set -euo pipefail
12
19
 
13
20
  PROFILE=code
21
+ HEADREF=""
22
+ CHANGED=""
14
23
  ARGS=()
15
24
  while [ $# -gt 0 ]; do
16
25
  case "$1" in
17
26
  --profile) PROFILE="${2:-code}"; shift 2 ;;
18
27
  --profile=*) PROFILE="${1#*=}"; shift ;;
28
+ --head) HEADREF="${2:-}"; shift 2 ;;
29
+ --head=*) HEADREF="${1#*=}"; shift ;;
30
+ --changed) CHANGED="${2:-}"; shift 2 ;;
31
+ --changed=*) CHANGED="${1#*=}"; shift ;;
19
32
  *) ARGS+=("$1"); shift ;;
20
33
  esac
21
34
  done
22
35
  case "$PROFILE" in code|hub) ;; *) echo "FAIL [pr-title]: unknown --profile '$PROFILE' (code|hub)."; exit 1 ;; esac
23
36
 
37
+ # True when the PR changes a front-half artifact (anything under epics/**). Reads the --changed list
38
+ # of paths CI computed from the PR diff; with no list (direct caller / test) it reports false.
39
+ artifact_changed() { [ -n "$CHANGED" ] && [ -f "$CHANGED" ] && grep -qE '^epics/' "$CHANGED"; }
40
+
24
41
  TITLE="${ARGS[0]:-}"
25
42
  if [ -z "$TITLE" ]; then
26
43
  echo "FAIL [pr-title]: empty title — pass the PR/MR title as the argument."
@@ -29,23 +46,43 @@ fi
29
46
 
30
47
  TYPES='feat|fix|docs|refactor|test|perf|build|ci|chore|revert'
31
48
 
32
- if [ "$PROFILE" = hub ]; then
33
- # review: <artifact> (EP-<slug>)
34
- if printf '%s' "$TITLE" | grep -qE '^review: .+ \(EP-[a-z0-9-]+\)$'; then
35
- echo "PASS [pr-title]: '${TITLE}' (profile: hub)"
36
- exit 0
49
+ # Conventional-Commits subject (optional scope + breaking `!`), no trailing period. Used by the code
50
+ # profile and by hub tooling PRs (any head branch that is not review/EP-*).
51
+ check_code_title() {
52
+ if ! printf '%s' "$TITLE" | grep -qE "^(${TYPES})(\([a-z0-9._-]+\))?!?: .+"; then
53
+ echo "FAIL [pr-title]: '${TITLE}' is not '<type>(<scope>)?!?: <description>' (type one of: ${TYPES//|/, })."
54
+ exit 1
37
55
  fi
38
- echo "FAIL [pr-title]: '${TITLE}' is not a hub review title 'review: <artifact> (EP-<slug>)'."
39
- exit 1
40
- fi
56
+ if printf '%s' "$TITLE" | grep -qE '\.$'; then
57
+ echo "FAIL [pr-title]: '${TITLE}' must not end with a period."
58
+ exit 1
59
+ fi
60
+ echo "PASS [pr-title]: '${TITLE}' (profile: ${PROFILE}, tooling/code)"
61
+ exit 0
62
+ }
41
63
 
42
- # code profile Conventional-Commits subject (optional scope + breaking `!`), no trailing period.
43
- if ! printf '%s' "$TITLE" | grep -qE "^(${TYPES})(\([a-z0-9._-]+\))?!?: .+"; then
44
- echo "FAIL [pr-title]: '${TITLE}' is not '<type>(<scope>)?!?: <description>' (type one of: ${TYPES//|/, })."
45
- exit 1
46
- fi
47
- if printf '%s' "$TITLE" | grep -qE '\.$'; then
48
- echo "FAIL [pr-title]: '${TITLE}' must not end with a period."
49
- exit 1
64
+ if [ "$PROFILE" = hub ]; then
65
+ # review/EP-* head branch (or unknown head ref) => front-half artifact-review PR: 'review: <artifact> (EP-<slug>)'.
66
+ case "$HEADREF" in
67
+ review/EP-*|"")
68
+ if printf '%s' "$TITLE" | grep -qE '^review: .+ \(EP-[a-z0-9-]+\)$'; then
69
+ echo "PASS [pr-title]: '${TITLE}' (profile: hub, artifact-review)"
70
+ exit 0
71
+ fi
72
+ echo "FAIL [pr-title]: '${TITLE}' is not a hub review title 'review: <artifact> (EP-<slug>)'."
73
+ exit 1
74
+ ;;
75
+ *)
76
+ # Any other hub PR is a tooling/code change to the hub itself — UNLESS it changes front-half
77
+ # artifacts (epics/**), which must go through a review/EP-* PR. Without this guard a non-review
78
+ # head could carry an artifact change past the front-half review with only a code title.
79
+ if artifact_changed; then
80
+ echo "FAIL [pr-title]: head '${HEADREF}' changes front-half artifacts (epics/**) but is not a review/EP-* branch — artifact changes must go through a review PR."
81
+ exit 1
82
+ fi
83
+ # tooling only — fall through to the code convention.
84
+ ;;
85
+ esac
50
86
  fi
51
- echo "PASS [pr-title]: '${TITLE}' (profile: code)"
87
+
88
+ check_code_title