yadflow 3.15.1 → 3.15.2

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,3 +1,10 @@
1
+ ## [3.15.2](https://github.com/abdelrahmannasr/yadflow/compare/v3.15.1...v3.15.2) (2026-08-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **checks:** exempt a new epic's ledger seed from ledger-guard ([ba923c2](https://github.com/abdelrahmannasr/yadflow/commit/ba923c2a3823e8bf17b2fc59b41f0160a3a11a19)), closes [#162](https://github.com/abdelrahmannasr/yadflow/issues/162)
7
+
1
8
  ## [3.15.1](https://github.com/abdelrahmannasr/yadflow/compare/v3.15.0...v3.15.1) (2026-08-11)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "3.15.1",
3
+ "version": "3.15.2",
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, thread, reconcile). A BMAD module + 38 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
@@ -138,6 +138,10 @@ Notes:
138
138
  - Also create an empty approvals ledger `{project-root}/epics/EP-<slug>/.sdlc/approvals.json`
139
139
  and an empty comments ledger `{project-root}/epics/EP-<slug>/.sdlc/comments.json`, each containing
140
140
  `[]`, and the `reviews/` directory.
141
+ - Commit the seed on the `analysis/EP-<slug>` branch, and cut `review/EP-<slug>/analysis` from it so the
142
+ epic's **first** review PR/MR carries the ledger to the default branch. In bridge mode `ledger-guard`
143
+ exempts a new epic's ledger (creation, not mutation, #162); every later change to it is CI's. See
144
+ `../yad-epic/references/state-schema.md`, "Authoring branches".
141
145
 
142
146
  ### Step 7 — Stop at the gate (do NOT advance)
143
147
  Report: epic ID, the path to `analysis.md`, and that the next action is **review** via
@@ -134,6 +134,12 @@ Seed `.sdlc/approvals.json` with one **provenance** record per inherited gate (N
134
134
  `{ "artifact": "<art>", "step": "<…-review>", "status": "inherited", "from": "<epic>", "boundHash": "<hash>", "date": "<today>" }`.
135
135
  Seed `.sdlc/comments.json` = `[]` and create `reviews/`.
136
136
 
137
+ Commit the seed on the `change/EP-<slug>` branch. It reaches the hub's default branch through this
138
+ change-epic's **first** review PR/MR — cut the `review/EP-<slug>/<artifact>` branch from `change/…` so
139
+ it carries the seed. In bridge mode `ledger-guard` exempts a new epic's ledger (creation, not mutation,
140
+ #162), so no direct push to a protected default branch is needed; every later change to that ledger is
141
+ CI's. See `../yad-epic/references/state-schema.md`, "Authoring branches".
142
+
137
143
  When `architecture` is **inherited**, materialize the **pointer-lock** `.sdlc/contract-lock.json`:
138
144
  `{ "artifact": "contract.md", "hash": "<parent surface hash, verbatim>", "lockedAt": "<today>", "inheritedFrom": "<epic>", "ref": "../../<epic>/.sdlc/contract-lock.json" }`.
139
145
  There is no `contract.md` in the change-epic, so the surface cannot drift, and `contract-check` passes
@@ -55,7 +55,11 @@ and GitLab CI. This step is **by hand** in Phase 3 — run the gates with the sk
55
55
  when humans legitimately own the ledger). On review PRs it FAILs any commit that touches the
56
56
  CI-owned gate ledger (`.sdlc/{state,approvals,comments,hub-prs}.json`, `reviews/*.md`) unless it
57
57
  is a **verified gate-bot commit** — bot-authored AND platform-Verified, since author text alone is
58
- spoofable. `.sdlc/contract-lock.json` is artifact-side and exempt. Runs in `yad-hub-checks`
58
+ spoofable. `.sdlc/contract-lock.json` is artifact-side and exempt. So is a **new epic's seed**:
59
+ no CI path can create a ledger (`gate ci` only *advances* an existing chain, at merge, on the
60
+ default branch), so an epic whose `.sdlc/state.json` is absent from the base ref may be created by
61
+ a human on its first review PR/MR — **creation, not mutation** (#162). Once the ledger is on the
62
+ default branch the guard is absolute again. Runs in `yad-hub-checks`
59
63
  alongside `verified-commits` (which waives the allowlist for the bot but still requires its
60
64
  signature). See `yad-hub-bridge`.
61
65
  - `templates/github/yad-verified-commits.yml` + `templates/gitlab/yad-verified-commits.gitlab-ci.yml`
@@ -11,6 +11,8 @@
11
11
  # NOT protected:
12
12
  # epics/*/.sdlc/contract-lock.json — artifact-side: the architect locks the contract surface in
13
13
  # `gate open`, so a human legitimately commits it alongside the architecture artifact.
14
+ # A brand-new epic's ledger — CREATION is not mutation (#162). No CI path can seed one, so the
15
+ # seed rides the first review PR/MR; see the carve-out below. Mutation stays bot-only.
14
16
  #
15
17
  # A "bot commit" must be BOTH authored by the gate bot (name/email contains yad-gate-sync) AND
16
18
  # platform-VERIFIED — author/committer text alone is user-controlled and spoofable, so the platform
@@ -114,19 +116,81 @@ trusted_bot() {
114
116
  signature_verified "$1"
115
117
  }
116
118
 
119
+ # ---- seeding carve-out: creation is not mutation (#162) ---------------------------------------
120
+ # A brand-new epic's ledger has no CI author. `gate ci` only ADVANCES an existing chain — it bails on
121
+ # a missing state.json ("the review branch is cut from the default branch, so it should carry it") and
122
+ # writes only at merge, on the default branch — and the engine itself reads a missing state.json as
123
+ # "not seeded yet". `gate open` writes nothing in bridge mode, and `checkpoint` stages back-half
124
+ # ledgers only. So the seed the authoring skills write (yad-epic / yad-change / yad-analysis /
125
+ # yad-discovery / yad-stub) can reach the trunk ONLY through the first review PR/MR — the one place
126
+ # this gate runs. Guarding it there makes the documented flow unshippable on a protected trunk, so an
127
+ # epic whose ledger is absent from the BASE ref is exempt: that ledger is human-authored by
128
+ # construction and the reviewer sees the whole of it in the diff. The moment its state.json is on the
129
+ # base ref the guard is absolute again.
130
+ #
131
+ # Anchored on state.json — the ledger root, the same "is this epic seeded?" question the engine asks —
132
+ # NOT on the individual file: adding hub-prs.json to an epic that IS on the base ref still FAILs.
133
+ # Anchored on the BASE ref, not the parent commit, so delete-then-re-add cannot reset the exemption
134
+ # (the deletion is itself a guarded change).
135
+ #
136
+ # The slug match is CASE-FOLDED on purpose. Git paths are byte-exact, but macOS/Windows checkouts are
137
+ # not: seeding `epics/ep-x/.sdlc/state.json` beside an on-base `epics/EP-X/` would probe as a brand-new
138
+ # epic, pass, and then land ON TOP of the real ledger in every case-insensitive clone — a mutation
139
+ # laundered as a creation. So the base's seeded slugs are read once (one ls-tree, not one probe per
140
+ # path) and compared folded.
141
+ #
142
+ # Slugs are held in ARRAYS read from NUL-delimited git output, never in a space- or newline-delimited
143
+ # string: git permits a newline inside a path, and a `EP-<newline>x` slug split across two records
144
+ # would drop the real epic out of the on-base list and let a mutation through as a "creation". Both
145
+ # arrays carry one empty sentinel element so `"${a[@]}"` is safe under `set -u` on bash 3.2 (macOS),
146
+ # which has no associative arrays; a slug is never empty, so the sentinel can never match.
147
+ base_slugs=("") # every epic with a ledger on BASE, lowercased
148
+ base_slugs_loaded=0
149
+ noted_slugs=("") # slugs already announced, so the note prints once each
150
+ fold() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
151
+ in_list() { # $1 = needle, $2… = haystack
152
+ _needle="$1"; shift
153
+ for _item in "$@"; do [ "$_item" = "$_needle" ] && return 0; done
154
+ return 1
155
+ }
156
+ is_seeding() { # $1 = epic slug; 0 when that epic has no ledger on BASE
157
+ if [ "$base_slugs_loaded" = 0 ]; then
158
+ while IFS= read -r -d '' _p; do
159
+ case "$_p" in
160
+ epics/*/.sdlc/state.json)
161
+ _s="${_p#epics/}"; _s="${_s%%/*}"
162
+ base_slugs[${#base_slugs[@]}]="$(fold "$_s")"
163
+ ;;
164
+ esac
165
+ done < <(git -c core.quotePath=false ls-tree -r --name-only -z "${BASE}" -- epics 2>/dev/null || true)
166
+ base_slugs_loaded=1
167
+ fi
168
+ _f="$(fold "$1")"
169
+ in_list "$_f" "${base_slugs[@]}" && return 1
170
+ in_list "$_f" "${noted_slugs[@]}" && return 0
171
+ noted_slugs[${#noted_slugs[@]}]="$_f"
172
+ echo "note [ledger-guard]: epics/$1 has no ledger on ${BASE} — new epic, its seed is exempt (creation, not mutation)."
173
+ return 0
174
+ }
175
+
117
176
  violations=0
118
177
  for sha in $commits; do
119
178
  touches_ledger=0
120
- while IFS= read -r f; do
179
+ # quotePath=false + -z on both git reads: a path git chose to escape ("epics/EP-caf\303\251/…") and a
180
+ # path holding a newline both match no arm below, so the gate would fail OPEN on exactly the paths it
181
+ # is meant to guard. NUL is the only byte a git path cannot contain.
182
+ while IFS= read -r -d '' f; do
121
183
  [ -n "$f" ] || continue
122
184
  case "$f" in
123
185
  epics/*/.sdlc/contract-lock.json) ;; # artifact-side — allowed
124
186
  epics/*/.sdlc/state.json|epics/*/.sdlc/approvals.json|epics/*/.sdlc/comments.json|epics/*/.sdlc/hub-prs.json|epics/*/reviews/*.md)
187
+ _slug="${f#epics/}"; _slug="${_slug%%/*}"
188
+ is_seeding "$_slug" && continue # a new epic's seed — not a mutation of a CI-owned ledger
125
189
  touches_ledger=1
126
190
  echo " ${sha} (author $(git show -s --format='%an' "$sha")) → $f"
127
191
  ;;
128
192
  esac
129
- done < <(git diff-tree --no-commit-id --name-only -r "$sha")
193
+ done < <(git -c core.quotePath=false diff-tree --no-commit-id --name-only -r -z "$sha")
130
194
  if [ "$touches_ledger" = 1 ] && ! trusted_bot "$sha"; then
131
195
  violations=$((violations + 1))
132
196
  fi
@@ -134,7 +198,18 @@ done
134
198
 
135
199
  if [ "$violations" -gt 0 ]; then
136
200
  echo "FAIL [ledger-guard]: ${violations} commit(s) change CI-owned gate files without a verified gate-bot signature. The ledger is CI-owned — let CI sync the gate; do not commit .sdlc/*.json or reviews/*.md yourself."
201
+ # An epic's seed is exempt only while its ledger is off the base ref. Once the first review PR merges
202
+ # (squashed or rebased, so the SHAs differ), those same seed commits still sitting on a sibling
203
+ # authoring branch read as mutations — the author did nothing wrong and the remedy is a rebase, so
204
+ # name it rather than leaving them with "do not commit the ledger yourself".
205
+ echo "hint [ledger-guard]: if these are seed commits from an already-merged review PR, rebase this branch onto the updated ${BASE} — the seed carve-out applies only until the ledger is on the base ref."
137
206
  exit 1
138
207
  fi
139
- echo "PASS [ledger-guard]: every CI-owned gate change in ${RANGE} is a verified gate-bot commit."
208
+ # Say WHICH rule passed the range: claiming "every change is a bot commit" would be false on a range
209
+ # whose only ledger change was a human seed the carve-out let through.
210
+ if [ "${#noted_slugs[@]}" -gt 1 ]; then # >1: the sentinel element is always there
211
+ echo "PASS [ledger-guard]: every CI-owned gate change in ${RANGE} is a verified gate-bot commit or a new epic's seed."
212
+ else
213
+ echo "PASS [ledger-guard]: every CI-owned gate change in ${RANGE} is a verified gate-bot commit."
214
+ fi
140
215
  exit 0
@@ -55,7 +55,9 @@ jobs:
55
55
  body="$(mktemp)"; printf '%s' "$PR_BODY" > "$body"
56
56
  bash checks/pr-template.sh --profile hub --head "$PR_HEAD" --changed "$changed" "$body"
57
57
 
58
- # The gate ledger is CI-owned: reject non-bot commits to .sdlc/*.json or reviews/*.md.
58
+ # The gate ledger is CI-owned: reject non-bot commits to .sdlc/{state,approvals,comments,hub-prs}.json
59
+ # or reviews/*.md (.sdlc/contract-lock.json is artifact-side and allowed). The one other exception is a
60
+ # brand-new epic's seed, which no CI path can write, so it rides this first review PR (#162).
59
61
  ledger-guard:
60
62
  runs-on: ubuntu-latest
61
63
  if: github.event.action != 'edited'
@@ -44,7 +44,9 @@ yad-hub-pr-template:
44
44
  - body="$(mktemp)"; printf '%s' "$CI_MERGE_REQUEST_DESCRIPTION" > "$body"
45
45
  - bash checks/pr-template.sh --profile hub --head "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --changed "$changed" "$body"
46
46
 
47
- # The gate ledger is CI-owned: reject non-bot commits to .sdlc/*.json or reviews/*.md.
47
+ # The gate ledger is CI-owned: reject non-bot commits to .sdlc/{state,approvals,comments,hub-prs}.json
48
+ # or reviews/*.md (.sdlc/contract-lock.json is artifact-side and allowed). The one other exception is a
49
+ # brand-new epic's seed, which no CI path can write, so it rides this first review MR (#162).
48
50
  yad-hub-ledger-guard:
49
51
  extends: .yad_hub_mr_only
50
52
  needs: []
@@ -112,6 +112,10 @@ Notes:
112
112
  never escalates to domain owners (no contract surface is touched yet).
113
113
  - Also create an empty approvals ledger `.sdlc/approvals.json` and comments ledger
114
114
  `.sdlc/comments.json`, each containing `[]`, and the `reviews/` directory.
115
+ - Commit the seed on the `discovery/EP-discovery` branch, and cut `review/EP-discovery/discovery` from
116
+ it so the **first** review PR/MR carries the ledger to the default branch. In bridge mode
117
+ `ledger-guard` exempts a new epic's ledger (creation, not mutation, #162); every later change to it
118
+ is CI's. See `../yad-epic/references/state-schema.md`, "Authoring branches".
115
119
 
116
120
  ### Step 6 — Stop at the gate (do NOT advance)
117
121
  Report: the path to the discovery set, and that the next action is **review** via `yad-review-gate`
@@ -152,6 +152,10 @@ Notes:
152
152
  - `test-cases` / `test-cases-review` are a **parallel, non-blocking track**: they seed `blocked` and open
153
153
  when `stories-review` passes — at which point the epic is already `ready-for-build`, so the build half
154
154
  runs alongside the tester. They never gate `ready-for-build` (see `references/state-schema.md`).
155
+ - Commit the seed on this step's authoring branch. It reaches the hub's default branch through the
156
+ epic's **first** review PR/MR — cut `review/EP-<slug>/epic` from the authoring branch so it carries
157
+ the seed. In bridge mode `ledger-guard` exempts a new epic's ledger (creation, not mutation, #162);
158
+ every later change to it is CI's. See `references/state-schema.md`, "Authoring branches".
155
159
  - Also create an empty approvals ledger `{project-root}/epics/EP-<slug>/.sdlc/approvals.json`
156
160
  and an empty comments ledger `{project-root}/epics/EP-<slug>/.sdlc/comments.json`, each containing
157
161
  `[]`, and the `reviews/` directory. (`comments.json` is the machine-readable counterpart to the
@@ -94,6 +94,16 @@ The shared procedure (run once the `EP-<slug>` is known):
94
94
  hub's default branch (`git checkout -b <step>/EP-<slug>`).
95
95
  3. Author and commit the step's artifact(s) on that branch. The bridge's `review/…` branch is created
96
96
  separately at review time and is untouched by this step.
97
+
98
+ **How the seed reaches the default branch.** The `.sdlc/` ledger is seeded once, by hand, on the
99
+ **entry** step's authoring branch (`analysis/…`, `epic/…`, `change/…`, `discovery/…`) — no CLI or CI
100
+ path creates one (`yad gate ci` only *advances* an existing chain, at merge, on the default branch).
101
+ So for the **first** gate of an epic, cut `review/EP-<slug>/<artifact-base>` from that authoring
102
+ branch: the review PR/MR then carries the seed alongside the artifact, and the ledger lands on the
103
+ default branch when it merges. In bridge mode `ledger-guard` exempts exactly this case — **creation,
104
+ not mutation** (#162) — so no direct push to a protected default branch is needed. For every **later**
105
+ gate the ledger is already on the default branch: cut the review branch from there, commit the
106
+ artifact only, and leave `.sdlc/{state,approvals,comments,hub-prs}.json` and `reviews/*.md` to CI.
97
107
  | `type` | `author` \| `review+approve` | Authoring step or a team review gate. |
98
108
  | `artifact` | filename or folder | The file/folder this step produces or gates. |
99
109
  | `assistance` | `none` \| `review` \| `heavy` | Dial 1 — how much AI helps (build plan §2). |
@@ -52,7 +52,10 @@ each required domain-owner to a platform `login` via the roster (a roster `name`
52
52
  ### Step 2 — `open` (create the review PR/MR)
53
53
  1. From the hub default branch, create `review/EP-<slug>/<artifact-base>` and ensure the artifact file
54
54
  (and, for architecture, `contract.md` + `.sdlc/contract-lock.json`) is committed on it. Push as the
55
- local user.
55
+ local user. **First gate of a new epic:** cut the review branch from the **authoring** branch
56
+ (`epic/…`, `change/…`, `analysis/…`, `discovery/…`) instead, so it carries the `.sdlc/` **seed** —
57
+ that PR/MR is the only way a new epic's ledger reaches the default branch, and `ledger-guard`
58
+ exempts it (see step 4).
56
59
  2. Open the PR/MR with `gh`/`glab` using the hub body template (`yad-pr-template` `templates/hub/…`),
57
60
  filled with the epic, artifact, gate step, owner, `epic.repos`, and the step's risk tags.
58
61
  3. **Request the required reviewers** (their logins) and add a `domain:<repo>` label per touched repo so
@@ -68,7 +71,9 @@ each required domain-owner to a platform `login` via the roster (a roster `name`
68
71
  A human commit touching the gate-state files (`.sdlc/{state,approvals,comments,hub-prs}.json` or
69
72
  `reviews/*.md`; `.sdlc/contract-lock.json` is artifact-side and allowed) on a review PR is rejected
70
73
  by the `ledger-guard` check. (The `yad gate open` CLI behaves the same: in bridge mode it opens the
71
- PR only and writes no ledger.)
74
+ PR only and writes no ledger.) The **one** exception is a brand-new epic's **seed** — no CI path can
75
+ create a ledger, so an epic whose `.sdlc/state.json` is absent from the base ref may be created by a
76
+ human on this first PR/MR (#162). Every later change to it is CI's alone.
72
77
  5. Report the PR/MR URL and the required reviewers. **Do not** record approvals or advance — reviewers
73
78
  act on the platform; CI (`yad gate ci`) reconciles it onto the default branch at merge.
74
79
 
@@ -143,7 +148,8 @@ default branch. (File-only mode keeps `yad gate sync` as the local writer.)
143
148
  - **Protect the hub default branch.** Require that `epics/**` artifacts change only through a review
144
149
  PR/MR (branch protection). This keeps revoke-on-change sound: it removes the only window where a
145
150
  delayed reconcile could advance on an out-of-band post-merge artifact change (see `references/bridge.md`,
146
- "Known limitation").
151
+ "Known limitation"). Safe to require: a new epic's `.sdlc/` seed rides its first review PR/MR, so
152
+ nobody needs a direct push to the default branch to start an epic (#162).
147
153
  - **Degrade gracefully.** No platform / disabled bridge / no CLI → the gate runs file-only with no error.
148
154
 
149
155
  ## Reference
@@ -155,7 +155,9 @@ During review CI writes nothing: the platform PR/MR is the source of truth (nati
155
155
  threads). The CLI is self-sufficient at merge: it derives the epic + artifact from the
156
156
  `review/EP-<slug>/<artifact-base>` head branch, takes the PR/MR number from the event (GitHub) or
157
157
  resolves it from the platform (GitLab), upserts the `hub-prs.json` entry itself, and **re-reads
158
- approvals fresh from the platform** — so no ledger needs to be pre-seeded on the branch.
158
+ approvals fresh from the platform** — so no ledger needs to be pre-seeded on the branch. (It only
159
+ *advances* a chain, though: it cannot **create** one. A brand-new epic's seed therefore travels the
160
+ other way — up through its first review PR/MR; see "the seed of a new epic" below.)
159
161
 
160
162
  | Platform event | Phase | CI action |
161
163
  |---|---|---|
@@ -174,13 +176,26 @@ commit — the advance plus the `draft → approved` status flip — lands on th
174
176
  check (yad-checks) FAILs any commit on a review PR that touches `.sdlc/{state,approvals,comments,hub-prs}
175
177
  .json` or `reviews/*.md` (`.sdlc/contract-lock.json` is artifact-side and allowed). Under Path B **no
176
178
  CI commit lands in a review PR at all**, so the only ledger change the guard can see there is a human
177
- edit — which it rejects. (The `verified-commits` gate still vets every commit's signature + author;
179
+ edit — which it rejects, with one carve-out for a new epic's seed (below). (The `verified-commits`
180
+ gate still vets every commit's signature + author;
178
181
  its gate-bot exemption is now vestigial in-PR because CI no longer commits there.) `yad gate open`
179
182
  opens the PR only; local `yad gate sync` is advisory in bridge mode (writes nothing). After a merge,
180
183
  everyone `git checkout <default> && git pull`. (Without the bridge, humans own the ledger locally and
181
184
  these guards are no-ops.)
182
185
 
183
- **The one sanctioned human ledger write: `yad gate repair`.** It heals a `YAD-STATE-005` chain (an
186
+ **The one sanctioned human ledger write *in a review PR*: the seed of a new epic.** `gate ci` only
187
+ **advances** an existing chain — it bails on a missing `state.json`, and the engine reads that absence
188
+ as "not seeded yet" — so no CI path can ever create a ledger. The seed the authoring skills write
189
+ (`yad-epic`, `yad-change`, `yad-analysis`, `yad-discovery`, `yad-stub`) can reach the default branch
190
+ only through the epic's **first** review PR/MR, which is exactly where `ledger-guard` runs. So the gate
191
+ exempts **creation**: an epic whose `.sdlc/state.json` is absent from the PR's base ref may have its
192
+ ledger written by a human there (#162). It stays a narrow carve-out — the probe is against the base
193
+ ref, not the parent commit, so deleting an on-trunk `state.json` to "re-seed" it is itself a rejected
194
+ mutation, and the moment the ledger is on the default branch every further change is CI's alone. Cut
195
+ that first review branch from the authoring branch (`epic/…`, `change/…`) so it carries the seed; no
196
+ direct push to a protected default branch is needed.
197
+
198
+ **The one sanctioned human ledger write *on the default branch*: `yad gate repair`.** It heals a `YAD-STATE-005` chain (an
184
199
  authoring step stranded behind a review gate that already advanced) by writing `state.json` alone. This
185
200
  is not a `ledger-guard` gap: the repair commits to the **default branch**, where `ledger-guard` — which
186
201
  only inspects review PRs — never runs, and where the `yad-update-guard` (platform-Verified signature +
@@ -176,7 +176,9 @@ PR only — against the `review/<epic>/<artifact>` branch, which must already ex
176
176
  records this skill describes. The skill's
177
177
  job is the human half: presenting the artifact, helping the owner address comments, and narrating the
178
178
  gate. Local `yad gate sync` is advisory in bridge mode (reads the platform, prints status, writes
179
- nothing); a human must never commit gate-state files (the `ledger-guard` check rejects it).
179
+ nothing); a human must never commit gate-state files (the `ledger-guard` check rejects it). The single
180
+ exception is an epic's **seed** — no CI path can create a ledger, so a brand-new epic's `.sdlc/` rides
181
+ its **first** review PR/MR, cut from the authoring branch (creation, not mutation, #162).
180
182
 
181
183
  Under that CLI the gate **advances on merge**: a review PR/MR whose reviewer rule is satisfied, whose
182
184
  comment threads are **all resolved**, and which has been **merged** auto-marks the step `done` and
@@ -115,6 +115,11 @@ into normal authoring with zero re-seeding.
115
115
  Also create the empty ledgers `{.sdlc/approvals.json}` and `{.sdlc/comments.json}` (each `[]`) and the
116
116
  `reviews/` directory. **Do NOT** write a `contract-lock.json` — a stub has no locked surface yet.
117
117
 
118
+ Commit the seed on this step's authoring branch; it reaches the hub's default branch through the
119
+ epic's **first** review PR/MR (or, for a stub, the PR that carries the stub itself). In bridge mode
120
+ `ledger-guard` exempts a new epic's ledger — creation, not mutation (#162) — while every later change
121
+ to it stays CI's. See `../yad-epic/references/state-schema.md`, "Authoring branches".
122
+
118
123
  ### Step 6 — Stop; hand off (NO auto-advance)
119
124
  Report the new `EP-<slug>`, that it is a **stub (backfill pending)**, and the two next moves:
120
125
  - **File bugs now:** `yad-change` (`--parent EP-<slug>`, `kind: defect|change`) — the defect threads off