yadflow 3.19.0-next.2 → 4.0.0-next.3

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.
Files changed (158) hide show
  1. package/CHANGELOG.md +371 -0
  2. package/README.md +160 -22
  3. package/bin/commands.mjs +46 -0
  4. package/bin/yad.mjs +506 -117
  5. package/cli/artifact-status.mjs +30 -11
  6. package/cli/capture.mjs +417 -0
  7. package/cli/checkpoint.mjs +47 -27
  8. package/cli/claims.mjs +165 -0
  9. package/cli/codeowners-command.mjs +170 -0
  10. package/cli/codeowners.mjs +397 -0
  11. package/cli/commit.mjs +13 -9
  12. package/cli/dial.mjs +183 -0
  13. package/cli/docs.mjs +88 -32
  14. package/cli/doctor.mjs +1682 -90
  15. package/cli/epic-state.mjs +3460 -211
  16. package/cli/epic.mjs +506 -0
  17. package/cli/errors.mjs +3 -0
  18. package/cli/fold.mjs +247 -0
  19. package/cli/gate-local.mjs +296 -0
  20. package/cli/gate.mjs +977 -183
  21. package/cli/history.mjs +556 -0
  22. package/cli/hook.mjs +264 -53
  23. package/cli/hubcommit.mjs +9 -19
  24. package/cli/index-command.mjs +87 -0
  25. package/cli/ledger.mjs +54 -4
  26. package/cli/lib.mjs +161 -9
  27. package/cli/manifest.mjs +376 -38
  28. package/cli/migrate.mjs +683 -52
  29. package/cli/mode.mjs +170 -0
  30. package/cli/next.mjs +365 -86
  31. package/cli/openpr.mjs +193 -34
  32. package/cli/owners.mjs +266 -0
  33. package/cli/people.mjs +657 -0
  34. package/cli/plan.mjs +466 -125
  35. package/cli/platform.mjs +109 -128
  36. package/cli/product-index.mjs +287 -0
  37. package/cli/protection.mjs +706 -0
  38. package/cli/reconcile.mjs +40 -12
  39. package/cli/repo-publish.mjs +24 -26
  40. package/cli/repo.mjs +23 -14
  41. package/cli/report.mjs +24 -14
  42. package/cli/review.mjs +19 -22
  43. package/cli/riskmap-command.mjs +289 -0
  44. package/cli/riskmap.mjs +373 -0
  45. package/cli/setup.mjs +127 -279
  46. package/cli/ship.mjs +6 -5
  47. package/cli/skill.mjs +180 -0
  48. package/cli/skip.mjs +211 -30
  49. package/cli/thread.mjs +41 -16
  50. package/cli/tidy.mjs +18 -18
  51. package/cli/update-commit.mjs +21 -21
  52. package/cli/usage.mjs +115 -109
  53. package/package.json +3 -3
  54. package/skills/sdlc/config.yaml +149 -70
  55. package/skills/sdlc/module-help.csv +24 -24
  56. package/skills/yad-analysis/SKILL.md +121 -59
  57. package/skills/yad-architecture/SKILL.md +30 -18
  58. package/skills/yad-architecture/references/contract-format.md +8 -5
  59. package/skills/yad-backfill/SKILL.md +10 -4
  60. package/skills/yad-backfill/references/backfill.md +8 -1
  61. package/skills/yad-backfill/templates/checks/backfill-check.sh +10 -2
  62. package/skills/yad-change/SKILL.md +119 -42
  63. package/skills/yad-change/references/triage.md +42 -27
  64. package/skills/yad-checks/SKILL.md +68 -35
  65. package/skills/yad-checks/references/check-gates.md +310 -85
  66. package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
  67. package/skills/yad-checks/templates/checks/contract-check.sh +130 -9
  68. package/skills/yad-checks/templates/checks/epic-open.sh +2 -2
  69. package/skills/yad-checks/templates/checks/ledger-guard.sh +55 -8
  70. package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
  71. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +1 -1
  72. package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
  73. package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
  74. package/skills/yad-checks/templates/github/yad-checks.yml +13 -2
  75. package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
  76. package/skills/yad-checks/templates/github/yad-update-guard.yml +7 -5
  77. package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
  78. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +10 -1
  79. package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
  80. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +3 -3
  81. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
  82. package/skills/yad-checks/templates/hooks/ledger-guard.sh +37 -6
  83. package/skills/yad-checks/templates/hooks/yad-capture.sh +53 -0
  84. package/skills/yad-commit/SKILL.md +4 -4
  85. package/skills/yad-connect-design/SKILL.md +5 -5
  86. package/skills/yad-connect-design/references/design-context.md +1 -1
  87. package/skills/yad-connect-design/references/design-registry.md +2 -2
  88. package/skills/yad-connect-docs/SKILL.md +11 -11
  89. package/skills/yad-connect-docs/references/docs-registry.md +1 -1
  90. package/skills/yad-connect-learning/SKILL.md +5 -5
  91. package/skills/yad-connect-learning/references/learning-registry.md +2 -2
  92. package/skills/yad-connect-repos/SKILL.md +66 -45
  93. package/skills/yad-connect-repos/references/code-context.md +4 -4
  94. package/skills/yad-connect-repos/references/hub-config.md +47 -51
  95. package/skills/yad-connect-repos/references/repos-registry.md +8 -7
  96. package/skills/yad-connect-repos/references/risk-map.md +81 -0
  97. package/skills/yad-connect-testing/SKILL.md +5 -5
  98. package/skills/yad-connect-testing/references/testing-context.md +3 -4
  99. package/skills/yad-connect-testing/references/testing-registry.md +2 -2
  100. package/skills/yad-defects/SKILL.md +7 -7
  101. package/skills/yad-discovery/SKILL.md +131 -94
  102. package/skills/yad-discovery/references/discovery-schema.md +22 -6
  103. package/skills/yad-discovery/references/foundation-schema.md +374 -0
  104. package/skills/yad-docs/SKILL.md +13 -8
  105. package/skills/yad-docs/references/data-mapping.md +9 -7
  106. package/skills/yad-docs/templates/app/package-lock.json +3 -3
  107. package/skills/yad-docs-overview/SKILL.md +30 -15
  108. package/skills/yad-docs-overview/references/pipeline-model.md +33 -20
  109. package/skills/yad-docs-sync/SKILL.md +10 -5
  110. package/skills/yad-docs-sync/references/staleness.md +8 -7
  111. package/skills/yad-engineer-review/SKILL.md +81 -17
  112. package/skills/yad-engineer-review/references/ship-and-record.md +17 -8
  113. package/skills/yad-epic/SKILL.md +173 -92
  114. package/skills/yad-epic/references/state-schema.md +610 -94
  115. package/skills/yad-hub-bridge/SKILL.md +57 -38
  116. package/skills/yad-hub-bridge/references/bridge.md +94 -67
  117. package/skills/yad-hub-bridge/references/login-roster.md +163 -70
  118. package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
  119. package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
  120. package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
  121. package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +21 -11
  122. package/skills/yad-implement/SKILL.md +27 -13
  123. package/skills/yad-implement/references/implement-conventions.md +2 -2
  124. package/skills/yad-learn/SKILL.md +9 -9
  125. package/skills/yad-learn/references/learning-state.md +2 -2
  126. package/skills/yad-open-pr/SKILL.md +61 -26
  127. package/skills/yad-pair-review/SKILL.md +7 -5
  128. package/skills/yad-pair-review/references/session-state.md +1 -1
  129. package/skills/yad-pr-template/SKILL.md +47 -26
  130. package/skills/yad-pr-template/references/risk-routing.md +97 -24
  131. package/skills/yad-pr-template/templates/checks/pr-template.sh +34 -11
  132. package/skills/yad-pr-template/templates/checks/pr-title.sh +32 -9
  133. package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
  134. package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
  135. package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
  136. package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +14 -13
  137. package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +14 -12
  138. package/skills/yad-reconcile/SKILL.md +3 -3
  139. package/skills/yad-report/SKILL.md +5 -5
  140. package/skills/yad-review-companion/SKILL.md +6 -3
  141. package/skills/yad-review-gate/SKILL.md +203 -93
  142. package/skills/yad-review-gate/references/gating.md +232 -54
  143. package/skills/yad-run/SKILL.md +81 -51
  144. package/skills/yad-run/references/run-loop.md +61 -39
  145. package/skills/yad-ship/SKILL.md +15 -11
  146. package/skills/yad-spec/SKILL.md +21 -6
  147. package/skills/yad-spec/references/spec-handoff.md +17 -5
  148. package/skills/yad-status/SKILL.md +115 -50
  149. package/skills/yad-stories/SKILL.md +37 -21
  150. package/skills/yad-stories/references/story-schema.md +7 -6
  151. package/skills/yad-stub/SKILL.md +58 -47
  152. package/skills/yad-sync-repos/SKILL.md +2 -2
  153. package/skills/yad-test-cases/SKILL.md +32 -23
  154. package/skills/yad-test-cases/references/test-cases-schema.md +7 -4
  155. package/skills/yad-timeline/SKILL.md +8 -7
  156. package/skills/yad-ui/SKILL.md +41 -18
  157. package/cli/roster.mjs +0 -164
  158. package/skills/sdlc/install.sh +0 -68
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: yad-hub-bridge
3
- description: 'The templated PR/MR bridge for the Shape review gate. When the product hub has a platform (.sdlc/hub.json), it opens a review PR/MR on the hub for an authored artifact (the optional analysis / epic / architecture+contract / ui-design / stories / test-cases), sets the required reviewers/labels from the routing rule, and provides the read-only gh/glab recipes that yad-review-gate uses to pull platform comments + approvals back into the file ledger. Can also wire merge-time sync on the hub: a CI workflow that runs `yad gate ci` when a human merges a review PR/MR — CI is the sole ledger writer and writes only at merge, on the default branch (during review the platform PR/MR is the source of truth; CI never touches the review branch). Local-user auth only — no stored tokens. The file ledger stays the source of truth; degrades to the local gate when there is no platform / no CLI. Use when the user says "open the review PR", "route the review", "wire the gate sync", or it is invoked by yad-review-gate open/sync.'
3
+ description: 'The templated PR/MR bridge for the Shape review gate. When the Product has a platform (.sdlc/hub.json), it opens a review PR/MR on the Product for an authored artifact (the optional analysis / epic / architecture+contract / ui-design / stories / test-cases), applies the touched-domain labels (it requests no reviewers), and provides the read-only gh/glab recipes that yad-review-gate uses to pull platform comments + approvals back into the file ledger. Can also wire merge-time sync on the Product: a CI workflow that runs `yad gate ci` when a human merges a review PR/MR — CI is the sole ledger writer and writes only at merge, on the default branch (during review the platform PR/MR is the source of truth; CI never touches the review branch). Local-user auth only — no stored tokens. The file ledger stays the source of truth; degrades to the local gate when there is no platform / no CLI. Use when the user says "open the review PR", "route the review", "wire the gate sync", or it is invoked by yad-review-gate open/sync.'
4
4
  ---
5
5
 
6
6
  # SDLC — Hub Review Bridge (the templated PR/MR bridge)
7
7
 
8
- **Goal:** Run the Shape review/comment/approval cycle through a **real PR/MR on the product hub**,
8
+ **Goal:** Run the Shape review/comment/approval cycle through a **real PR/MR on the Product**,
9
9
  without changing the gate's predicate or making the file ledger optional. The verified ledger is an **alternate
10
10
  input path** into `yad-review-gate`: it opens a review PR for an artifact, reviewers approve/comment on
11
11
  the platform with **their own** `gh`/`glab` auth, and the gate's `sync` action (which calls this skill's
@@ -17,12 +17,13 @@ keeps platform mechanics out of the gate). `yad-review-gate` *calls* it; it neve
17
17
 
18
18
  ## Conventions
19
19
 
20
- - `{project-root}` is the **product hub**. Hub platform + reviewer roster live in `.sdlc/hub.json`
21
- (`config.yaml` `hub.config`; schema in `../yad-connect-repos/references/hub-config.md`).
22
- - Per-step review-PR record: `epics/EP-<slug>/.sdlc/hub-prs.json` (`config.yaml` `hub.pr_ledger`) — a
20
+ - `{project-root}` is the **Product**. The Product platform lives in `.sdlc/hub.json` — it holds no
21
+ list of people (`config.yaml` `product.config` (older projects: `hub.config`); schema in `../yad-connect-repos/references/hub-config.md`).
22
+ - Per-step review-PR record: `epics/EP-<slug>/.sdlc/product-prs.json`, written together with its
23
+ older name `hub-prs.json` (`config.yaml` `product.pr_ledger` (older projects: `hub.pr_ledger`)) — a
23
24
  sibling ledger to `approvals.json`, so `state.json`'s locked step shape is untouched.
24
- - The review-PR body is the hub template from `yad-pr-template` (`templates/hub/<platform>/…`).
25
- - Branch per artifact: `review/EP-<slug>/<artifact-base>` (`config.yaml` `hub.artifact_branch`).
25
+ - The review-PR body is the Product template from `yad-pr-template` (`templates/hub/<platform>/…`).
26
+ - Branch per artifact: `review/EP-<slug>/<artifact-base>` (`config.yaml` `product.artifact_branch` (older projects: `hub.artifact_branch`)).
26
27
  - **Local-user auth only; store no tokens.** Use the user's own `gh`/`glab`. If neither is installed/
27
28
  authenticated, STOP this path and tell the gate to fall back to local — never embed a credential.
28
29
 
@@ -35,31 +36,40 @@ keeps platform mechanics out of the gate). `yad-review-gate` *calls* it; it neve
35
36
 
36
37
  ## Preconditions (the verified ledger runs only when all hold)
37
38
 
38
- `.sdlc/hub.json` exists with a non-null `platform`, `ledger: "verified"` (or, before `yad migrate`, `bridge_enabled: true`), `config.yaml` `hub.bridge:
39
- true`, and `gh` (GitHub) / `glab` (GitLab) installed **and authenticated**. If any fails, report that the
39
+ `.sdlc/hub.json` exists with a non-null `platform`, `ledger: "verified"` (or, before `yad migrate`, `bridge_enabled: true`), `config.yaml` `product.bridge: true`
40
+ (older projects: `hub.bridge: true`), and `gh` (GitHub) / `glab` (GitLab) installed **and authenticated**. If any fails, report that the
40
41
  gate proceeds **local** (no error) and stop.
41
42
 
42
43
  ## On Activation
43
44
 
44
45
  ### Step 1 — Resolve platform + routing
45
- Read `.sdlc/hub.json` for the platform and roster, `epics/<epic>/epic.md` for `repos` + `owner`, and the
46
- matching `review+approve` step's `risk_tags` from `.sdlc/state.json`. Compute the **required reviewers**
47
- with `route` (below) — the same rule `yad-review-gate` enforces: base (owner + 1 reviewer); escalated
48
- (`risk_tags` ∩ {contract,auth,payments}, or the stories step) adds a domain-owner per touched repo. Map
49
- each required domain-owner to a platform `login` via the roster (a roster `name` equal to a repo's
50
- `domain_owner` in `repos.json` is that repo's domain-owner).
46
+ Read `.sdlc/hub.json` for the platform, `epics/<epic>/epic.md` for `repos` + `owner` (the artifact's
47
+ author), and the matching `review+approve` step's `risk_tags` from `.sdlc/state.json`. Compute the
48
+ **count** with `route` (below) — the same rule `yad-review-gate` enforces: `needed = base 1 + risk step`
49
+ distinct approvers, where `contract` adds 2 and `auth`/`payments` add 1 (the highest tag, never the sum).
50
+ Only the base (1 distinct approver, who should not be the author) holds the gate; the risk step is
51
+ advisory. The gate caps the count at the active people less one and prints it (E72), but does
52
+ not enforce it yet; when it cannot count the people — as in Product CI, which checks out only the hub —
53
+ no cap is shown. Compute the **touched domains** too: the union of story `repos` for
54
+ `stories-review`, the epic's `repos` for a step tagged `contract`, `auth` or `payments`, none otherwise.
55
+ They become `domain:<repo>` labels and
56
+ are a hint for whom to ask. There is no roster to turn them into people.
51
57
 
52
58
  ### Step 2 — `open` (create the review PR/MR)
53
- 1. From the hub default branch, create `review/EP-<slug>/<artifact-base>` and ensure the artifact file
54
- (and, for architecture, `contract.md` + `.sdlc/contract-lock.json`) is committed on it. Push as the
59
+ 1. From the Product default branch, create `review/EP-<slug>/<artifact-base>` and ensure the artifact file
60
+ (and, for architecture, `contract.md` + `.sdlc/contract-lock.json`) is committed on it — by the step's
61
+ `yad fold` commit (E44); `yad open-pr` warns when the step's files have changes no fold has committed. Push as the
55
62
  local user. **First gate of a new epic:** cut the review branch from the **authoring** branch
56
63
  (`epic/…`, `change/…`, `analysis/…`, `discovery/…`) instead, so it carries the `.sdlc/` **seed** —
57
64
  that PR/MR is the only way a new epic's ledger reaches the default branch, and `ledger-guard`
58
65
  exempts it (see step 4).
59
- 2. Open the PR/MR with `gh`/`glab` using the hub body template (`yad-pr-template` `templates/hub/…`),
66
+ 2. Open the PR/MR with `gh`/`glab` using the Product body template (`yad-pr-template` `templates/hub/…`),
60
67
  filled with the epic, artifact, gate step, owner, `epic.repos`, and the step's risk tags.
61
- 3. **Request the required reviewers** (their logins) and add a `domain:<repo>` label per touched repo so
62
- per-repo routing is legible on the PR.
68
+ 3. **Request no reviewers.** Assign the person opening it — `@me` on GitHub (`gh` resolves it), the
69
+ login `glab api user` reports on GitLab (no assignee when that lookup fails) — and add a
70
+ `domain:<repo>` label per touched repo. `yad gate open` prints
71
+ `no reviewers were requested — ask them on the PR itself`; the author asks people on the PR. (A Shape review PR gets no
72
+ reviewer suggestion: E68's suggestion reads a code change, so only `yad open-pr` (and `yad ship`) on a code repo prints it.)
63
73
  4. **Do not write the ledger.** CI is the sole writer and writes only at merge. During review nothing
64
74
  is recorded in the ledger — the platform PR/MR holds the review state (native approvals + threads).
65
75
  At merge, CI records the `hub-prs.json` entry (in the shape below) and advances on the default
@@ -68,22 +78,27 @@ each required domain-owner to a platform `login` via the roster (a roster `name`
68
78
  { "step": "<review step id>", "artifact": "<artifact>", "platform": "github|gitlab",
69
79
  "number": <n>, "url": "<pr/mr url>", "branch": "review/EP-<slug>/<artifact-base>", "lastSyncedAt": null }
70
80
  ```
71
- A human commit touching the gate-state files (`.sdlc/{state,approvals,comments,hub-prs}.json` or
81
+ A human commit touching the gate-state files (`.sdlc/{state,approvals,comments,product-prs,hub-prs}.json` or
72
82
  `reviews/*.md`; `.sdlc/contract-lock.json` is artifact-side and allowed) on a review PR is rejected
73
83
  by the `ledger-guard` check. (The `yad gate open` CLI behaves the same: in verified mode it opens the
74
84
  PR only and writes no ledger.) The **one** exception is a brand-new epic's **seed** — no CI path can
75
85
  create a ledger, so an epic whose `.sdlc/state.json` is absent from the base ref may be created by a
76
86
  human on this first PR/MR (#162). Every later change to it is CI's alone.
77
- 5. Report the PR/MR URL and the required reviewers. **Do not** record approvals or advance — reviewers
87
+ 5. Report the PR/MR URL and the count the step needs. **Do not** record approvals or advance — reviewers
78
88
  act on the platform; CI (`yad gate ci`) reconciles it onto the default branch at merge.
79
89
 
80
- ### Step 3 — `route` (print required reviewers)
81
- Compute and print the required reviewers as above. Use `templates/checks/hub-route.sh <body>` to parse a
82
- PR/MR body's Impact & Risk block when given one; otherwise derive from `epic.repos` + the step's
83
- `risk_tags`. Advisory only — it routes the human review, it does not approve.
84
-
85
- ### Step 4 — `wire` (merge-time sync on the hub)
86
- Install the hub CI that turns the human **merge** into a `yad gate ci` run, with CI as the **sole
90
+ ### Step 3 — `route` (print the count)
91
+ Compute and print the count as above. Use `templates/checks/hub-route.sh <body>` to parse a PR/MR
92
+ body's Impact & Risk block when given one; it prints e.g. `ROUTE: 3 approvers = base 1 + contract risk 2
93
+ (risk tag: contract)`, says only the base holds the gate (1 approval), that the risk step is advisory
94
+ and `yad gate status` prints it capped by the active people, and lists the touched repos as a hint for whom
95
+ to ask. With no `contract`/`auth`/`payments` tag it prints `ROUTE: 1 approver = base 1 (no risk step).`
96
+ and no repo list. Otherwise derive from
97
+ `epic.repos` + the step's `risk_tags`. Stories do not route per repo. Advisory only — it routes the
98
+ human review, it does not approve.
99
+
100
+ ### Step 4 — `wire` (merge-time sync on the Product)
101
+ Install the Product CI that turns the human **merge** into a `yad gate ci` run, with CI as the **sole
87
102
  writer** of the ledger. There is no pre-merge CI write — during review the platform PR/MR is the
88
103
  source of truth (native approvals + threads). On merge, CI re-reads approvals from the platform,
89
104
  advances the step, and flips the artifact `status:` on the **default branch** (the only place CI ever
@@ -103,12 +118,15 @@ default branch. (local mode keeps `yad gate sync` as the local writer.)
103
118
  enabled bridge in `.sdlc/hub.json` it installs
104
119
  - GitHub → `.github/workflows/yad-gate-sync.yml` (from `templates/github/yad-gate-sync.yml`)
105
120
  - GitLab → `.gitlab/ci/yad-gate-sync.yml` (from `templates/gitlab/yad-gate-sync.gitlab-ci.yml`)
106
- - plus the hub-side **verified-commits** gate (`checks/verified-commits.sh` + its workflow/fragment,
107
- owned by `yad-checks`) so review PRs accept only signed commits from roster-known authors
121
+ - plus the Product-side **verified-commits** gate (`checks/verified-commits.sh` + its workflow/fragment,
122
+ owned by `yad-checks`) so review PRs accept only platform-Verified (signed) commits — there is no
123
+ author allowlist
108
124
  - the wired job runs an **exact** version, resolved at run time from the repo: the `YAD_VERSION`
109
125
  variable, else `hub.json` `gate_sync_version`, else the `.sdlc/cli-version.json` stamp, else the
110
- floating `3`. It stopped floating on the major because that let a release change a scheduled job's
111
- behaviour unattended — how #163's churn reached hubs that never opted in. So upgrading is now a
126
+ floating major the fragment ships with (`YAD_MAJOR`, `4` in this release) — a committed pin is used
127
+ only when it is an exact release of that major, so `yad update` moves the fragment and the stamp
128
+ to a new major together. It stopped floating on the major because that let a release change a scheduled job's
129
+ behaviour unattended — how #163's churn reached Products that never opted in. So upgrading is now a
112
130
  deliberate act (`yad update`); if the resolved pin predates 3.15.3, upgrade or disable the
113
131
  schedule. Never edit a version into the wired file — `yad check --fix` rewrites it byte-for-byte
114
132
  from the template. See `references/bridge.md`.
@@ -128,7 +146,7 @@ default branch. (local mode keeps `yad gate sync` as the local writer.)
128
146
  case set the variable to a real tag. The variable lives in project settings, so it survives
129
147
  every `yad` sync. Single value only — `tags: [$VAR]` is one tag equal to the whole variable,
130
148
  not a comma-split.
131
- 3. Commit the workflow to the hub. GitHub needs nothing else — the ephemeral `github.token` reads the
149
+ 3. Commit the workflow to the Product. GitHub needs nothing else — the ephemeral `github.token` reads the
132
150
  PR and pushes the merge advance to the default branch, and the workflow's reconcile **schedule**
133
151
  runs automatically (no setup) as the safety net that recovers a merge whose run failed transiently.
134
152
  If the default branch is protected, see the workflow header for the bypass / PAT options; until then
@@ -143,7 +161,8 @@ default branch. (local mode keeps `yad gate sync` as the local writer.)
143
161
  is the source of truth** and the gate predicate (in `yad-review-gate`) is unchanged.
144
162
  - **the verified ledger never approves on a reviewer's behalf.** Reviewers approve/merge with their own auth. The
145
163
  step advances when a human **merges** the approved, fully-resolved review PR (the merge is that human
146
- act) — `yad gate sync` records the approvals + resolution + merged state and advances; unresolved
164
+ act) — `yad gate ci` at merge (or `yad gate sync` with a local ledger) records the approvals +
165
+ resolution + merged state and advances; unresolved
147
166
  comments or a changed artifact hold it `in_review`. The mechanical sync is the `yad gate` CLI.
148
167
  - **CI never approves and never merges.** The wired workflow only runs `gate ci` — the same sync +
149
168
  unchanged predicate. It does **nothing pre-merge** (the platform PR/MR holds the review state); at
@@ -154,7 +173,7 @@ default branch. (local mode keeps `yad gate sync` as the local writer.)
154
173
  ephemeral `github.token` (nothing stored). GitLab requires a stored masked `SDLC_GATE_TOKEN`
155
174
  project access token because `CI_JOB_TOKEN` can neither read the approvals API nor push — say so
156
175
  when wiring, and scope it to `read_api` + `write_repository` only.
157
- - **Protect the hub default branch.** Require that `epics/**` artifacts change only through a review
176
+ - **Protect the Product default branch.** Require that `epics/**` artifacts change only through a review
158
177
  PR/MR (branch protection). This keeps revoke-on-change sound: it removes the only window where a
159
178
  delayed reconcile could advance on an out-of-band post-merge artifact change (see `references/bridge.md`,
160
179
  "Known limitation"). Safe to require: a new epic's `.sdlc/` seed rides its first review PR/MR, so
@@ -164,6 +183,6 @@ default branch. (local mode keeps `yad gate sync` as the local writer.)
164
183
  ## Reference
165
184
  - PR-body→ledger mapping, the read-only gh/glab recipes, idempotent re-sync, contract re-lock handling:
166
185
  `references/bridge.md`.
167
- - Roster schema, login→role resolution, unverified-login fallback, per-repo routing: `references/login-roster.md`.
186
+ - How approvals, comments and records are attributed to a platform login (no roster): `references/login-roster.md`.
168
187
  - The gate this feeds (open hook + sync action + unchanged predicate): `../yad-review-gate/SKILL.md`, `../yad-review-gate/references/gating.md`.
169
- - The hub PR/MR body template + the code-repo routing analogue: `../yad-pr-template/`.
188
+ - The Product PR/MR body template + the code-repo routing analogue: `../yad-pr-template/`.
@@ -8,8 +8,8 @@ predicate (`../yad-review-gate/references/gating.md`) runs unchanged. The verifi
8
8
 
9
9
  | Platform review state | Ledger effect |
10
10
  |---|---|
11
- | GitHub review `APPROVED` / GitLab MR approval (`approved_by`) | an `approved` record in `approvals.json`, role resolved from the roster (owner/reviewer) or derived domain-owner, tagged `"source": "bridge"` |
12
- | GitHub `COMMENTED` / `CHANGES_REQUESTED`; GitLab discussions/notes | a line under `## <name> (<role>)` in `reviews/<artifact>--<date>--comments.md` + a `comments.json` record; **never** an approval. `CHANGES_REQUESTED` is also flagged as blocking in the comments file |
11
+ | GitHub review `APPROVED` / GitLab MR approval (`approved_by`) | one `approved` record per person in `approvals.json`, `approver` = the platform login, no role, tagged `"source": "bridge"` |
12
+ | GitHub `COMMENTED` / `CHANGES_REQUESTED`; GitLab discussions/notes | a line under `## <login> (unresolved)` or `## <login> (changes requested — **blocking**)` in `reviews/<artifact>--<date>--comments.md` + a `comments.json` record (`commenter` = the login); **never** an approval. `CHANGES_REQUESTED` is also flagged as blocking in the comments file |
13
13
  | GitHub review dismissed / GitLab approval revoked | the prior bridge `approved` record for that approver is removed on re-sync **while the step is open**; once the step is `done` the record is kept as the audit trail of why it passed (see idempotency) |
14
14
 
15
15
  `approvals.json` records from the verified ledger carry `"source": "bridge"`; **manual** approvals have no such
@@ -23,13 +23,13 @@ gh pr view <n> --json reviews,comments,reviewDecision,latestReviews
23
23
  gh api repos/{owner}/{repo}/pulls/{n}/comments # inline review comments
24
24
  ```
25
25
  - `reviews[].state` ∈ {APPROVED, CHANGES_REQUESTED, COMMENTED, DISMISSED}; `reviews[].author.login` is
26
- the login to resolve. Use `latestReviews` so a superseded earlier review doesn't double-count.
26
+ the login written to the record. Use `latestReviews` so a superseded earlier review doesn't double-count.
27
27
 
28
28
  **GitLab** (`glab` / `glab api`):
29
29
  ```
30
30
  glab mr view <n>
31
31
  glab api projects/:id/merge_requests/:iid/approvals # approved_by[].user.username
32
- glab api projects/:id/merge_requests/:iid/notes # discussion notes (comments)
32
+ glab api projects/:id/merge_requests/:iid/discussions # discussions + their notes (comments, resolvable threads)
33
33
  ```
34
34
 
35
35
  All commands run as the **local user**; the verified ledger stores no tokens. If the CLI is missing/unauthenticated
@@ -41,70 +41,81 @@ or the remote is unreachable, the verified ledger stops and the gate falls back
41
41
  > reviewer state. (GitHub maps both.) If you need GitLab "Request changes" honored, read
42
42
  > `reviewers[].state` from the MR and map it to `CHANGES_REQUESTED`.
43
43
 
44
- ## Open recipes (request the reviewers — used by `yad gate open` / `yad open-pr`)
44
+ ## Open recipes (no reviewers requested — used by `yad gate open` / `yad open-pr`)
45
45
 
46
- Opening the review PR/MR must **request the required reviewers**, or an escalated gate is opened with
47
- nobody asked. The CLI (`createPr` in `cli/platform.mjs`) does this; an agent opening a PR by hand uses:
46
+ Opening the review PR/MR **requests no reviewers**. yadflow keeps no list of people to pick them from
47
+ (E62), so the command prints `no reviewers were requested — ask them on the PR itself`, and the author
48
+ asks people on the PR. (On a code-repo PR, `yad open-pr` also prints a reviewer suggestion from recent history and CODEOWNERS — E68, a hint, never a request.) `yad gate open` assigns the opener and adds the touched-domain labels (`domain:<repo>`); `yad open-pr`
49
+ assigns the opener and adds no labels. An agent opening a PR by hand
50
+ uses:
48
51
 
49
- **GitHub** — create, then add each reviewer (a bad/non-collaborator login WARNS instead of aborting the
50
- whole create):
52
+ **GitHub:**
51
53
  ```bash
52
54
  gh pr create --title "review: <artifact> (<epic>)" --body <body> --base <default> --head <branch> \
53
- --assignee @me --label domain:<repo>
54
- gh pr edit <n> --add-reviewer <login> # once per required reviewer
55
+ --assignee <login|@me> --label domain:<repo>
55
56
  ```
56
57
 
57
- **GitLab** — a Free/Core MR carries a **single** reviewer field (multiple reviewers is Premium), so
58
- assign the first required reviewer and **@-mention the rest in a note** so they are still notified/routed:
58
+ **GitLab:**
59
59
  ```bash
60
60
  glab mr create --title "review: <artifact> (<epic>)" --description <body> \
61
- --target-branch <default> --source-branch <branch> --reviewer <first-login> --label domain:<repo> --yes
62
- glab mr note <iid> -m "Review requested (owner + reviewer rule): @<l2> @<l3> — please review and approve/comment on this MR (this drives the gate)."
61
+ --target-branch <default> --source-branch <branch> --assignee <login> --label domain:<repo> --yes
63
62
  ```
64
- The read side counts a mentioned reviewer normally: their eventual **approval** still appears in
65
- `…/approvals → approved_by[]`, and their **note** in `…/discussions` — so the single-reviewer-field cap
66
- loses only the native "Reviewers" UI chip, not the gate routing.
67
63
 
68
- Required reviewers = the hub's `reviewer`/`domain-owner` roster logins for the touched scopes, PLUS any
69
- repo whose ownership lives only in `repos.json` `domain_owner`/`domain_owners` (those are resolved to a
70
- login and requested too — otherwise an escalated step is structurally unsatisfiable through routing).
64
+ The assignee is `@me` on GitHub (`gh` resolves it on the repo's host) and the login `glab api user`
65
+ reports on GitLab (none when that lookup fails). The `domain:<repo>` labels are the touched domains —
66
+ for `stories-review` the union of every story's `repos`, for a step tagged `contract`, `auth` or
67
+ `payments` the epic's `repos`, and none otherwise. They are a hint for whom to ask and add no
68
+ approvals.
71
69
 
72
- ## Login → role resolution (order)
70
+ ## Who a record names
73
71
 
74
- 1. Roster (`.sdlc/hub.json`) maps `login` → `name` + base `role` (owner/reviewer).
75
- 2. If that `name` equals a repo's `domain_owner` in `repos.json` **and** that repo is a touched domain
76
- for this step → also emit a `domain-owner` record with `domain: <repo>`.
77
- 3. Login not in the roster → `name: <login>`, `role: reviewer`, flagged
78
- `<!-- unverified login: <login> -->`. **Never** auto-promoted to owner/domain-owner.
72
+ The platform login that reviewed is written as `approver` / `commenter`. There is no lookup and no
73
+ role, and no login is flagged `unverified`: the platform's record of who approved is the evidence, and
74
+ repository access decides who may approve. A review with no login is not counted.
79
75
 
80
- (Full detail + per-repo routing: `login-roster.md`.)
76
+ (Full detail, including older roster-era records and the `by` field: `login-roster.md`.)
81
77
 
82
78
  ## Idempotent re-sync
83
79
 
84
- - Key bridge approvals on `(step, approver, role, domain)`. On re-sync, **upsert** — do not append a
85
- duplicate.
80
+ - Key bridge approvals on `(step, approver)` — one record per person. On re-sync, **upsert** — do not
81
+ append a duplicate. An older record that still carries `role`/`domain` (and may name the person by
82
+ their old roster name) is recognised as `login-roster.md` → "Older records" describes (the roster's
83
+ name → login pairs first; never by list order), and replaced by one login-named record that keeps its
84
+ `artifactHash` and dates — unless the platform shows a newer review (a later submission time, with a
85
+ time on both sides, or a different PR/MR), which, as for any approval, binds to the current content.
86
+ Every sync write (`yad gate sync`, `yad gate ci`) also records the login on the older records the
87
+ roster can place for certain, on every step of the epic; only the records it cannot place still need the
88
+ roster on a later sync (`login-roster.md` → "Recording the login on older
89
+ records").
90
+ - A bridge approval records the platform's evidence when it is given: `approvedAt` (GitHub's submission
91
+ time, GitLab's `approved_at`), and on GitHub the review's `commit`, `url` and `reviewId`. They are
92
+ audit only — the gate decides on the login and the fingerprint. A stored `approvedAt` that is only a
93
+ date (every GitLab record from before this) is "time unknown": it keeps its fingerprint and takes the
94
+ platform's time once.
86
95
  - **On an OPEN step**, remove any bridge approval whose platform review was dismissed/revoked: the
87
96
  platform is the live source of truth while the review is in flight.
88
97
  - **On a step already `done`**, the record is only added to or refreshed in place — an approval the
89
98
  platform no longer reports is **kept**. Those approvals are the audit record of *why* the gate
90
- passed; a roster edit, an approval reset, or a degraded-but-successful read would otherwise erase
99
+ passed; an approval reset, or a degraded-but-successful read would otherwise erase
91
100
  them and leave the step `done` with zero approvals.
92
- - Key synced comments on the platform comment id so the same comment is not appended twice. Comment
93
- rounds are recorded for an **open** step only, so re-visiting a merged review does not append a new
94
- round per pass.
101
+ - Record synced comments as one `comments.json` record per `(step, commenter, round)` with a `count`
102
+ (there is no per-comment id). A new round is opened only when the commenters or their counts change;
103
+ an unchanged re-read rewrites the latest round in place, keeping its date. Comment rounds are recorded
104
+ for an **open** step only, so re-visiting a merged review does not append a new round per pass.
95
105
  - Update the step's `hub-prs.json` `lastSyncedAt` when the sync **learned something** — every sync on
96
106
  an open step, and on a closed one only when the approval record actually changed (a re-opened review
97
107
  that was re-approved). An identical re-sync leaves it alone, so the ledger does not churn.
98
- - **Write the ledgers in a canonical order.** `approvals.json`, `comments.json` and `hub-prs.json` are
108
+ - **Write the ledgers in a canonical order.** `approvals.json`, `comments.json` and the PR ledger
109
+ (`product-prs.json` + `hub-prs.json`, written together) are
99
110
  sorted on write, so the bytes are a function of the record *set* and never of which step was synced
100
111
  last. Without this the upsert above — which drops the records it refreshes and re-appends them at the
101
112
  tail — makes the file depend on sync order, and the wired sweep (one `gate ci --branch <ref>
102
113
  --merged` per merged PR/MR, every 15 minutes) walks a rotation:
103
114
  `[A,B,C] → sync A → [B,C,A] → sync B → [C,A,B] → sync C → [A,B,C]`. Every hop is a non-empty diff, so
104
115
  every hop commits and pushes, and the pass ends where it began — an unbounded commit loop with zero
105
- semantic change. That is issue #163: ~1,800 bot commits/day on the hub that reported it. Sorting is
116
+ semantic change. That is issue #163: ~1,800 bot commits/day on the Product that reported it. Sorting is
106
117
  what makes the "nothing staged → nothing to commit" guard in `gate ci` actually hold.
107
- - Running `sync` twice with no platform change is a no-op on the ledger — byte-identical, including
118
+ - Running `sync` twice on the same day with no platform change is a no-op on the ledger — byte-identical, including
108
119
  `comments.json` and the dated `reviews/*.md` side files.
109
120
 
110
121
  **Upgrading past #163.** The first sweep on a yadflow carrying the fix writes one canonicalizing
@@ -117,22 +128,24 @@ deliberate act: `yad update` (which re-stamps `.sdlc/cli-version.json`), or a `g
117
128
 
118
129
  ## Contract re-lock invalidates prior platform approvals too
119
130
 
120
- For the **architecture+contract** review, the gate already drops approvals when the contract-surface hash
121
- no longer matches `.sdlc/contract-lock.json`. The verified ledger extends this to platform-sourced approvals:
122
- `sync` discards bridge `approved` records for the architecture step dated **before** the new lock, and
123
- posts a comment on the review PR noting "contract re-locked — re-approval required". The escalation
124
- (`risk_tags: ["contract"]` → a domain-owner per repo) is unchanged.
131
+ For the **architecture+contract** review, an approval's `artifactHash` is the contract-surface hash.
132
+ When the surface changes and is re-locked, a bridge approval recorded against the old hash no longer
133
+ matches, so the gate stops counting it (it shows as stale in `yad gate status`) — exactly as for a
134
+ manual approval. Nothing is deleted by date and no comment is posted on the PR; the reviewer approves
135
+ again, and a genuinely newer review (a later submission time, or a new PR/MR) binds to the new hash. The count
136
+ (`risk_tags: ["contract"]` → 3 approvers, capped at the active people less one and reported; base 1
137
+ enforced, risk step advisory) is unchanged.
125
138
 
126
139
  ## CHANGES_REQUESTED & unresolved threads hold the gate
127
140
 
128
141
  `CHANGES_REQUESTED` and any **unresolved review thread** are recorded as comments and surfaced as
129
142
  **blocking**. Under the PR-driven gate they actively hold the step `in_review`: the predicate does not
130
- pass while any thread is unresolved, even if the approval counts are met. The owner addresses the
143
+ pass while any thread is unresolved, even if the approval count is met. The owner addresses the
131
144
  comments, replies, the reviewer **resolves** their thread, then `sync` runs again.
132
145
 
133
146
  ## Merge advances; an artifact change revokes approvals
134
147
 
135
- - **Merge → advance.** When the reviewer rule is satisfied, every thread is resolved, **and the review
148
+ - **Merge → advance.** When the base count is met (1 distinct approver; the capped risk step is reported, not enforced; solo mode waives it), every thread is resolved, **and the review
136
149
  PR/MR is merged**, `sync` marks the step `done` and unblocks the next step. The merge is the human
137
150
  approval act — there is no separate machine advance. (`yad gate sync` performs this deterministically.)
138
151
  - **Revoke on artifact change (checked at merge).** Path B reconciles at merge, so an approval given
@@ -151,27 +164,30 @@ comments, replies, the reviewer **resolves** their thread, then `sync` runs agai
151
164
  advancing on approvals whose freshness cannot be proven (re-run `yad gate sync` to recover).
152
165
  CHANGES_REQUESTED is still honored, so a degraded read can only ever *hold* the gate.
153
166
  The `artifactHash` stamp still binds architecture approvals to the locked contract surface (see
154
- "Contract re-lock" above).
155
- - **Known limitation — protect the hub default branch.** The advance hashes the artifact from the
167
+ "Contract re-lock" above). For every other artifact it covers the file WITHOUT its frontmatter
168
+ `status:` line, so the merge run's own `draft` → `approved` flip (and Build's `in-build` / `shipped`)
169
+ never revokes the approvals it just recorded (shape 9). An approval recorded before shape 9 hashed the
170
+ whole file, and is still read.
171
+ - **Known limitation — protect the Product default branch.** The advance hashes the artifact from the
156
172
  default branch as it stands when CI runs, while approvals are SHA-bound to the reviewed PR/MR head.
157
173
  Those can differ if the artifact changes on the **base** outside this review while the PR/MR is open
158
174
  (the merge then integrates a change the reviewers never saw) or if a later out-of-band commit edits
159
175
  the merged artifact before a delayed reconcile advances it. In both cases each approval's commit
160
176
  still equals the reviewed head, so the SHA check passes, yet the live content was not reviewed. Close
161
- it operationally: **require branch protection on the hub default branch so `epics/**` artifacts can
177
+ it operationally: **require branch protection on the Product default branch so `epics/**` artifacts can
162
178
  only change through their own review PR/MR** (one open review per artifact) — then the base copy of an
163
179
  artifact cannot move while its review is open, so the merged/live content always equals the reviewed
164
180
  content. (The complete in-code fix would hash the artifact at the reviewed PR-head revision before
165
181
  advancing; deferred in favor of the branch-protection mitigation.)
166
182
 
167
- ## Event-driven sync (hub CI) — Path B
183
+ ## Event-driven sync (Product CI) — Path B
168
184
 
169
- The `wire` action (SKILL.md Step 4) installs CI on the hub so a **merge** drives `yad gate ci` —
185
+ The `wire` action (SKILL.md Step 4) installs CI on the Product so a **merge** drives `yad gate ci` —
170
186
  **CI is the SOLE writer of the ledger, and it writes only at merge, only to the default branch.**
171
187
  During review CI writes nothing: the platform PR/MR is the source of truth (native approvals +
172
188
  threads). The CLI is self-sufficient at merge: it derives the epic + artifact from the
173
189
  `review/EP-<slug>/<artifact-base>` head branch, takes the PR/MR number from the event (GitHub) or
174
- resolves it from the platform (GitLab), upserts the `hub-prs.json` entry itself, and **re-reads
190
+ resolves it from the platform (GitLab), upserts the PR-ledger entry itself (under both names), and **re-reads
175
191
  approvals fresh from the platform** — so no ledger needs to be pre-seeded on the branch. (It only
176
192
  *advances* a chain, though: it cannot **create** one. A brand-new epic's seed therefore travels the
177
193
  other way — up through its first review PR/MR; see "the seed of a new epic" below.)
@@ -183,28 +199,38 @@ other way — up through its first review PR/MR; see "the seed of a new epic" be
183
199
  | Schedule (`*/15`) | reconcile | Safety net: enumerate recently-**merged** `review/EP-*` PRs/MRs via the API and advance any not yet `done` (idempotent). Recovers a merge whose merge-time run failed transiently, and on GitLab also picks up a squash merge whose commit dropped the branch name (and a bare approval — GitLab fires no pipeline on one). **GitHub:** a scheduled workflow, automatic once committed. **GitLab:** a pipeline schedule with `SDLC_GATE_SYNC=true` (one-time setup) |
184
200
 
185
201
  **Which yadflow the wired job runs.** Both fragments resolve the version from a `YAD_VERSION` variable
186
- and fall back to `3`:
202
+ and fall back to the major the fragment ships with — `YAD_MAJOR` in its `yad-pin` block (`4` in this release):
187
203
 
188
204
  | # | Source | Set it in |
189
205
  |---|---|---|
190
206
  | 1 | `YAD_VERSION` — used **verbatim**, the operator's override | GitHub: Settings → Secrets and variables → Actions → **Variables**. GitLab: Settings → CI/CD → **Variables** (beside `SDLC_GATE_TOKEN`) |
191
- | 2 | `.sdlc/hub.json` → `gate_sync_version` — this hub's committed pin | edit `hub.json`, commit it |
192
- | 3 | `.sdlc/cli-version.json` → `version` — the yadflow that last wired the hub | `yad update` re-stamps it |
193
- | 4 | `3` — floating major, only when nothing above resolves | — |
207
+ | 2 | `.sdlc/hub.json` → `gate_sync_version` — this Product's committed pin | edit `hub.json`, commit it |
208
+ | 3 | `.sdlc/cli-version.json` → `version` — the yadflow that last wired the Product | `yad update` re-stamps it |
209
+ | 4 | `$YAD_MAJOR` — floating major, only when nothing above resolves. While only prereleases of that major exist (`4.0.0-next.N`) npm cannot resolve a bare major, so the job says so on stderr first; a Product wired by that release always has a stamp that resolves | — |
194
210
 
195
- Sources 2 and 3 are **validated** before use: an exact `3.x.y` release token, prereleases included
196
- (`3.16.0-rc.1` is a legitimate pin; `latest` and a bare `3` are not). A `.sdlc/cli-version.json`
211
+ Sources 2 and 3 are **validated** before use: an exact release token of the fragment's own major, prereleases
212
+ included (`4.16.0-rc.1` is a legitimate pin; `latest`, a bare `4` and a `3.x` are not). A `.sdlc/cli-version.json`
197
213
  written by a long-untouched project can still say something like `1.0.2`, a version with no `yad gate ci`
198
214
  in it at all, and the value is interpolated into `npx -p "yadflow@$V"` on a runner holding a push token —
199
215
  so anything that is not an exact release of this major is skipped, loudly, in favour of the next source.
200
216
  `YAD_VERSION` is exempt: it is a human's deliberate act, and it is the only way to cross a major.
201
217
 
218
+ **A new major moves both files at once.** `yad update` rewrites the fragment from the release's template
219
+ and re-stamps `.sdlc/cli-version.json` in the same run, so a Product that updates to 4.x gets a fragment
220
+ with `YAD_MAJOR=4` and a `4.x` stamp together. A `gate_sync_version` pin in `hub.json` from the old major
221
+ is then SKIPPED (the job logs `ignoring pin … not an exact 4.x release`) — update it or remove it. A
222
+ fragment the team EDITED is kept by `yad update` while the stamp still moves, so an edited fragment on
223
+ the old major would skip the new stamp; `yad update` names that case, and `--overwrite-local` replaces
224
+ the fragment (saving the edit beside it). The
225
+ fragment's major is held to the major a release publishes by `scripts/pin-major-check.sh`, a step of the
226
+ release check.
227
+
202
228
  **Why this is no longer a floating major.** It used to be, on the argument that a published fix should
203
229
  reach a scheduled job with nobody in the loop — this page's own issue #163 as the example. The same
204
230
  mechanism is how #163's churn *arrived*: the CI fragment ran `yadflow@3`, so 3.13.1 rolled onto every
205
- wired hub automatically and took the reporting one from 20 to 96 churn commits an hour, with nobody
231
+ wired Product automatically and took the reporting one from 20 to 96 churn commits an hour, with nobody
206
232
  deciding to upgrade. Issue #163's fourth suggested fix was to stop that. The trade-off is real and cuts
207
- both ways — a hub is no longer carried onto a fix for free, so **if the resolved pin is older than
233
+ both ways — a Product is no longer carried onto a fix for free, so **if the resolved pin is older than
208
234
  3.15.3, run `yad update` or disable the schedule** (`yad doctor` flags a stale stamp).
209
235
 
210
236
  The pin is **never stamped into the wired file**: `yad` owns that file and `yad check --fix` rewrites it
@@ -220,15 +246,15 @@ commit — the advance plus the `draft → approved` status flip — lands on th
220
246
  `[skip ci]`.
221
247
 
222
248
  **The ledger is CI-owned (verified mode only).** Humans never commit gate-state files: the `ledger-guard`
223
- check (yad-checks) FAILs any commit on a review PR that touches `.sdlc/{state,approvals,comments,hub-prs}
249
+ check (yad-checks) FAILs any commit on a review PR that touches `.sdlc/{state,approvals,comments,product-prs,hub-prs}
224
250
  .json` or `reviews/*.md` (`.sdlc/contract-lock.json` is artifact-side and allowed). "verified mode" there
225
251
  means the same thing it means everywhere else — a `platform` **and** the verified ledger flag, `isVerifiedLedger`'s
226
- predicate. The gate used to enable itself on the flag alone, which let a platform-less hub reject the
252
+ predicate. The gate used to enable itself on the flag alone, which let a platform-less Product reject the
227
253
  human's ledger write while the CLI still expected one (#186). Under Path B **no
228
254
  CI commit lands in a review PR at all**, so the only ledger change the guard can see there is a human
229
255
  edit — which it rejects, with one carve-out for a new epic's seed (below). (The `verified-commits`
230
- gate still vets every commit's signature + author;
231
- its gate-bot exemption is now vestigial in-PR because CI no longer commits there.) `yad gate open`
256
+ gate still vets every commit's platform-Verified signature — there is no author allowlist, and so no
257
+ gate-bot exemption either.) `yad gate open`
232
258
  opens the PR only; local `yad gate sync` is advisory in verified mode (writes nothing). After a merge,
233
259
  everyone `git checkout <default> && git pull`. (with a local ledger, humans own the ledger locally and
234
260
  these guards are no-ops.)
@@ -248,8 +274,8 @@ direct push to a protected default branch is needed.
248
274
  **The one sanctioned human ledger write *on the default branch*: `yad gate repair`.** It heals a `YAD-STATE-005` chain (an
249
275
  authoring step stranded behind a review gate that already advanced) by writing `state.json` alone. This
250
276
  is not a `ledger-guard` gap: the repair commits to the **default branch**, where `ledger-guard` — which
251
- only inspects review PRs — never runs, and where the `yad-update-guard` (platform-Verified signature +
252
- roster-allowlisted author) vets it instead, exactly as it does for `yad checkpoint` and `yad update`.
277
+ only inspects review PRs — never runs, and where the `yad-update-guard` (platform-Verified signature)
278
+ vets it instead, exactly as it does for `yad checkpoint` and `yad update`.
253
279
  The command refuses to commit off the default branch unless `--allow-branch` is passed.
254
280
 
255
281
  **Loop prevention & races.** The only ledger commit lands on the **default branch** at merge, which
@@ -270,7 +296,8 @@ push retries with a rebase.
270
296
  Actions, else a fine-grained PAT as `SDLC_GATE_TOKEN` on the mergesync checkout.
271
297
 
272
298
  **Manual sync & recovery.** In verified mode `yad gate sync` is **advisory** (read-only) — it prints the
273
- predicate but writes nothing, so it is **not** a recovery path when CI fails. If a merge-time run fails
299
+ predicate (and any `! may not be met:` / `! if the risk step were enforced:` warning, E73) but writes
300
+ nothing, so it is **not** a recovery path when CI fails. If a merge-time run fails
274
301
  (can't push, API hiccup), recovery is the scheduled **reconcile** job (automatic; it re-advances merged
275
302
  reviews not yet `done`). To force it immediately, a maintainer runs the same command CI runs, locally on
276
303
  the default branch: `yad gate ci --branch <review-branch> --pr <n> --merged` (this writes + pushes,
@@ -302,7 +329,7 @@ sweep's recent-MR window overlaps whatever the merge-push pipeline is handling
302
329
  one at a time (`concurrency: yad-gate-mergesync` on GitHub, `resource_group: yad-gate-mergesync` on
303
330
  GitLab). Without it both runs produce the same advance, one pushes, and the other rebases onto it and
304
331
  lands a duplicate `chore(gate): advance …` commit under a different SHA — unreviewed churn, since
305
- those commits carry `[skip ci]` and go straight to the default branch. An already-wired GitLab hub
332
+ those commits carry `[skip ci]` and go straight to the default branch. An already-wired GitLab Product
306
333
  picks the `resource_group` up on the next `yad update` / `yad check --fix`.
307
334
 
308
335
  **`yad gate open` does not create or push the review branch.** It opens a PR/MR *against*
@@ -313,7 +340,7 @@ rather than failing inside `gh`/`glab`; an origin that cannot be reached at all
313
340
 
314
341
  ### Manual end-to-end verification (GitHub)
315
342
 
316
- 1. On a scratch hub: `yad setup` (platform github, roster with a second account) → `yad check --fix`
343
+ 1. On a scratch Product: `yad setup` (platform github; a second account with write access to review) → `yad check --fix`
317
344
  installs `.github/workflows/yad-gate-sync.yml`; commit + push it.
318
345
  2. Author an epic → `yad gate open EP-x epic.md` → the review PR opens. CI writes nothing yet — review
319
346
  state lives on the platform.