yadflow 3.18.1 → 4.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +355 -0
  2. package/README.md +79 -26
  3. package/bin/commands.mjs +41 -0
  4. package/bin/yad.mjs +437 -124
  5. package/cli/artifact-status.mjs +34 -15
  6. package/cli/checkpoint.mjs +69 -49
  7. package/cli/codeowners-command.mjs +170 -0
  8. package/cli/codeowners.mjs +397 -0
  9. package/cli/commit.mjs +13 -9
  10. package/cli/companion.mjs +2 -2
  11. package/cli/dial.mjs +183 -0
  12. package/cli/docs.mjs +88 -32
  13. package/cli/doctor.mjs +1472 -97
  14. package/cli/epic-state.mjs +3478 -232
  15. package/cli/epic.mjs +506 -0
  16. package/cli/errors.mjs +4 -1
  17. package/cli/gate.mjs +1002 -209
  18. package/cli/history.mjs +556 -0
  19. package/cli/hook.mjs +266 -55
  20. package/cli/hubcommit.mjs +6 -17
  21. package/cli/index-command.mjs +87 -0
  22. package/cli/ledger.mjs +57 -7
  23. package/cli/lib.mjs +184 -18
  24. package/cli/manifest.mjs +367 -56
  25. package/cli/migrate.mjs +726 -53
  26. package/cli/mode.mjs +170 -0
  27. package/cli/next.mjs +349 -90
  28. package/cli/openpr.mjs +191 -39
  29. package/cli/people.mjs +654 -0
  30. package/cli/plan.mjs +417 -132
  31. package/cli/platform.mjs +110 -129
  32. package/cli/product-index.mjs +287 -0
  33. package/cli/protection.mjs +706 -0
  34. package/cli/reconcile.mjs +38 -12
  35. package/cli/repo-publish.mjs +24 -26
  36. package/cli/repo.mjs +23 -14
  37. package/cli/report.mjs +21 -15
  38. package/cli/review.mjs +24 -27
  39. package/cli/riskmap-command.mjs +289 -0
  40. package/cli/riskmap.mjs +373 -0
  41. package/cli/setup.mjs +139 -287
  42. package/cli/ship.mjs +7 -6
  43. package/cli/skill.mjs +180 -0
  44. package/cli/skip.mjs +211 -30
  45. package/cli/thread.mjs +42 -17
  46. package/cli/tidy.mjs +20 -20
  47. package/cli/update-commit.mjs +22 -22
  48. package/cli/usage.mjs +115 -109
  49. package/package.json +3 -3
  50. package/skills/sdlc/config.yaml +166 -87
  51. package/skills/sdlc/module-help.csv +35 -35
  52. package/skills/yad-analysis/SKILL.md +125 -65
  53. package/skills/yad-architecture/SKILL.md +34 -23
  54. package/skills/yad-architecture/references/contract-format.md +10 -8
  55. package/skills/yad-backfill/SKILL.md +14 -8
  56. package/skills/yad-backfill/references/backfill.md +1 -1
  57. package/skills/yad-change/SKILL.md +127 -52
  58. package/skills/yad-change/references/triage.md +42 -28
  59. package/skills/yad-checks/SKILL.md +89 -45
  60. package/skills/yad-checks/references/check-gates.md +315 -92
  61. package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
  62. package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
  63. package/skills/yad-checks/templates/checks/contract-check.sh +58 -2
  64. package/skills/yad-checks/templates/checks/epic-open.sh +3 -3
  65. package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
  66. package/skills/yad-checks/templates/checks/ledger-guard.sh +94 -18
  67. package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
  68. package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
  69. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +4 -4
  70. package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
  71. package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
  72. package/skills/yad-checks/templates/github/yad-checks.yml +37 -5
  73. package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
  74. package/skills/yad-checks/templates/github/yad-update-guard.yml +3 -4
  75. package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
  76. package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
  77. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +22 -4
  78. package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
  79. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +4 -4
  80. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
  81. package/skills/yad-checks/templates/hooks/ledger-guard.sh +38 -7
  82. package/skills/yad-commit/SKILL.md +6 -6
  83. package/skills/yad-connect-design/SKILL.md +6 -6
  84. package/skills/yad-connect-design/references/design-context.md +1 -1
  85. package/skills/yad-connect-design/references/design-registry.md +2 -2
  86. package/skills/yad-connect-docs/SKILL.md +12 -12
  87. package/skills/yad-connect-docs/references/docs-registry.md +1 -1
  88. package/skills/yad-connect-learning/SKILL.md +5 -5
  89. package/skills/yad-connect-learning/references/learning-registry.md +2 -2
  90. package/skills/yad-connect-repos/SKILL.md +92 -54
  91. package/skills/yad-connect-repos/references/code-context.md +6 -6
  92. package/skills/yad-connect-repos/references/hub-config.md +68 -58
  93. package/skills/yad-connect-repos/references/repos-registry.md +10 -9
  94. package/skills/yad-connect-repos/references/risk-map.md +81 -0
  95. package/skills/yad-connect-testing/SKILL.md +6 -6
  96. package/skills/yad-connect-testing/references/testing-context.md +3 -4
  97. package/skills/yad-connect-testing/references/testing-registry.md +2 -2
  98. package/skills/yad-defects/SKILL.md +8 -8
  99. package/skills/yad-discovery/SKILL.md +130 -94
  100. package/skills/yad-discovery/references/discovery-schema.md +23 -7
  101. package/skills/yad-discovery/references/foundation-schema.md +374 -0
  102. package/skills/yad-docs/SKILL.md +16 -11
  103. package/skills/yad-docs/references/data-mapping.md +9 -7
  104. package/skills/yad-docs/templates/app/package-lock.json +3 -3
  105. package/skills/yad-docs-overview/SKILL.md +32 -17
  106. package/skills/yad-docs-overview/references/pipeline-model.md +47 -28
  107. package/skills/yad-docs-sync/SKILL.md +10 -5
  108. package/skills/yad-docs-sync/references/staleness.md +8 -7
  109. package/skills/yad-engineer-review/SKILL.md +88 -24
  110. package/skills/yad-engineer-review/references/ship-and-record.md +25 -16
  111. package/skills/yad-epic/SKILL.md +178 -100
  112. package/skills/yad-epic/references/state-schema.md +626 -117
  113. package/skills/yad-hub-bridge/SKILL.md +66 -48
  114. package/skills/yad-hub-bridge/references/bridge.md +110 -83
  115. package/skills/yad-hub-bridge/references/login-roster.md +163 -70
  116. package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
  117. package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
  118. package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
  119. package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +22 -12
  120. package/skills/yad-implement/SKILL.md +29 -15
  121. package/skills/yad-implement/references/implement-conventions.md +2 -2
  122. package/skills/yad-learn/SKILL.md +9 -9
  123. package/skills/yad-learn/references/learning-state.md +2 -2
  124. package/skills/yad-open-pr/SKILL.md +64 -29
  125. package/skills/yad-pair-review/SKILL.md +18 -16
  126. package/skills/yad-pair-review/references/session-state.md +4 -4
  127. package/skills/yad-pr-template/SKILL.md +48 -27
  128. package/skills/yad-pr-template/references/risk-routing.md +97 -24
  129. package/skills/yad-pr-template/templates/checks/pr-template.sh +37 -15
  130. package/skills/yad-pr-template/templates/checks/pr-title.sh +27 -13
  131. package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
  132. package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
  133. package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
  134. package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +15 -14
  135. package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +15 -13
  136. package/skills/yad-reconcile/SKILL.md +3 -3
  137. package/skills/yad-report/SKILL.md +5 -5
  138. package/skills/yad-review-companion/SKILL.md +12 -9
  139. package/skills/yad-review-gate/SKILL.md +198 -79
  140. package/skills/yad-review-gate/references/gating.md +230 -54
  141. package/skills/yad-run/SKILL.md +86 -56
  142. package/skills/yad-run/references/run-loop.md +67 -45
  143. package/skills/yad-ship/SKILL.md +18 -14
  144. package/skills/yad-spec/SKILL.md +31 -17
  145. package/skills/yad-spec/references/spec-handoff.md +17 -5
  146. package/skills/yad-status/SKILL.md +114 -56
  147. package/skills/yad-stories/SKILL.md +42 -27
  148. package/skills/yad-stories/references/story-schema.md +10 -9
  149. package/skills/yad-stub/SKILL.md +59 -48
  150. package/skills/yad-sync-repos/SKILL.md +3 -3
  151. package/skills/yad-test-cases/SKILL.md +37 -30
  152. package/skills/yad-test-cases/references/test-cases-schema.md +8 -5
  153. package/skills/yad-timeline/SKILL.md +8 -7
  154. package/skills/yad-ui/SKILL.md +46 -25
  155. package/cli/roster.mjs +0 -164
  156. package/skills/sdlc/install.sh +0 -68
@@ -1,18 +1,18 @@
1
- # The bridge — PR/MR ↔ ledger mapping, read recipes, idempotency
1
+ # the verified ledger — PR/MR ↔ ledger mapping, read recipes, idempotency
2
2
 
3
- The bridge maps platform review state onto the **same file records** the manual gate writes, so the gate
4
- predicate (`../yad-review-gate/references/gating.md`) runs unchanged. The bridge only changes the
3
+ the verified ledger maps platform review state onto the **same file records** the manual gate writes, so the gate
4
+ predicate (`../yad-review-gate/references/gating.md`) runs unchanged. The verified ledger only changes the
5
5
  *input path*; it never changes what passing the gate means.
6
6
 
7
7
  ## State mapping (platform → ledger)
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
- `approvals.json` records from the bridge carry `"source": "bridge"`; **manual** approvals have no such
15
+ `approvals.json` records from the verified ledger carry `"source": "bridge"`; **manual** approvals have no such
16
16
  tag and are **never** touched by `sync` — the two coexist.
17
17
 
18
18
  ## Read recipes (read-only, local-user auth — no tokens)
@@ -23,17 +23,17 @@ 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
- All commands run as the **local user**; the bridge stores no tokens. If the CLI is missing/unauthenticated
36
- or the remote is unreachable, the bridge stops and the gate falls back to file-only (no error).
35
+ All commands run as the **local user**; the verified ledger stores no tokens. If the CLI is missing/unauthenticated
36
+ or the remote is unreachable, the verified ledger stops and the gate falls back to local (no error).
37
37
 
38
38
  > **GitLab read parity (GAP-6).** `readPrGitLab` reads approvals (`approved_by[]`) and discussions but
39
39
  > does **not** map a "Request changes" reviewer state to `CHANGES_REQUESTED` — on GitLab the blocking
@@ -41,70 +41,81 @@ or the remote is unreachable, the bridge stops and the gate falls back to file-o
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 bridge 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
@@ -219,18 +245,18 @@ each `artifactHash` against the merged content, so revoke-on-change still holds.
219
245
  commit — the advance plus the `draft → approved` status flip — lands on the **default branch** with
220
246
  `[skip ci]`.
221
247
 
222
- **The ledger is CI-owned (bridge 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}
224
- .json` or `reviews/*.md` (`.sdlc/contract-lock.json` is artifact-side and allowed). "Bridge mode" there
225
- means the same thing it means everywhere else — a `platform` **and** the bridge flag, `isBridge`'s
226
- predicate. The gate used to enable itself on the flag alone, which let a platform-less hub reject the
248
+ **The ledger is CI-owned (verified mode only).** Humans never commit gate-state files: the `ledger-guard`
249
+ check (yad-checks) FAILs any commit on a review PR that touches `.sdlc/{state,approvals,comments,product-prs,hub-prs}
250
+ .json` or `reviews/*.md` (`.sdlc/contract-lock.json` is artifact-side and allowed). "verified mode" there
251
+ means the same thing it means everywhere else — a `platform` **and** the verified ledger flag, `isVerifiedLedger`'s
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`
232
- opens the PR only; local `yad gate sync` is advisory in bridge mode (writes nothing). After a merge,
233
- everyone `git checkout <default> && git pull`. (Without the bridge, humans own the ledger locally and
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`
258
+ opens the PR only; local `yad gate sync` is advisory in verified mode (writes nothing). After a merge,
259
+ everyone `git checkout <default> && git pull`. (with a local ledger, humans own the ledger locally and
234
260
  these guards are no-ops.)
235
261
 
236
262
  **The one sanctioned human ledger write *in a review PR*: the seed of a new epic.** `gate ci` only
@@ -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
@@ -269,12 +295,13 @@ push retries with a rebase.
269
295
  - Protected default branch (GitHub): the merge advance needs to push it — prefer a ruleset bypass for
270
296
  Actions, else a fine-grained PAT as `SDLC_GATE_TOKEN` on the mergesync checkout.
271
297
 
272
- **Manual sync & recovery.** In bridge 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
298
+ **Manual sync & recovery.** In verified mode `yad gate sync` is **advisory** (read-only) — it prints the
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,
277
- unlike advisory `yad gate sync`). File-only mode (no platform) keeps `yad gate sync` as the local writer.
304
+ unlike advisory `yad gate sync`). Local mode (no platform) keeps `yad gate sync` as the local writer.
278
305
  The file ledger is still the source of truth.
279
306
 
280
307
  Because CI records the `hub-prs.json` pointer only at merge, a review the ledger has never seen still
@@ -283,17 +310,17 @@ recorded pointer, `yad gate sync <epic> <artifact>` resolves the PR/MR from the
283
310
  (`review/<epic>/<artifact>`) itself, and `--pr <n>` names it outright:
284
311
 
285
312
  ```bash
286
- yad gate sync EP-x architecture.md --pr 42 # advisory in bridge mode; the writer without the bridge
313
+ yad gate sync EP-x architecture.md --pr 42 # advisory in verified mode; the writer with a local ledger
287
314
  ```
288
315
 
289
316
  An explicit `--pr` also **overrides** a recorded pointer, since a re-opened review is a new PR the
290
317
  ledger has not seen. Before its reviewers are bound to the artifact's hash, the number is confirmed to
291
318
  be the PR for that artifact's review branch — a mismatch is refused, and a platform that cannot answer
292
- warns and proceeds. The bridge rule is unchanged: the resolved pointer is adopted into the ledger only
319
+ warns and proceeds. The verified ledger rule is unchanged: the resolved pointer is adopted into the ledger only
293
320
  on the writer path, so a human never leaves a gate-state file in their working tree for `ledger-guard`
294
321
  to reject.
295
322
 
296
- **In bridge mode `gate sync` stays advisory even with `--pr`** — it prints the predicate and writes
323
+ **In verified mode `gate sync` stays advisory even with `--pr`** — it prints the predicate and writes
297
324
  nothing, because CI owns the ledger. The recovery that actually writes is the command CI itself runs,
298
325
  on the default branch: `yad gate ci --branch <review-branch> --pr <n> --merged`.
299
326
 
@@ -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.