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.
- package/CHANGELOG.md +355 -0
- package/README.md +79 -26
- package/bin/commands.mjs +41 -0
- package/bin/yad.mjs +437 -124
- package/cli/artifact-status.mjs +34 -15
- package/cli/checkpoint.mjs +69 -49
- package/cli/codeowners-command.mjs +170 -0
- package/cli/codeowners.mjs +397 -0
- package/cli/commit.mjs +13 -9
- package/cli/companion.mjs +2 -2
- package/cli/dial.mjs +183 -0
- package/cli/docs.mjs +88 -32
- package/cli/doctor.mjs +1472 -97
- package/cli/epic-state.mjs +3478 -232
- package/cli/epic.mjs +506 -0
- package/cli/errors.mjs +4 -1
- package/cli/gate.mjs +1002 -209
- package/cli/history.mjs +556 -0
- package/cli/hook.mjs +266 -55
- package/cli/hubcommit.mjs +6 -17
- package/cli/index-command.mjs +87 -0
- package/cli/ledger.mjs +57 -7
- package/cli/lib.mjs +184 -18
- package/cli/manifest.mjs +367 -56
- package/cli/migrate.mjs +726 -53
- package/cli/mode.mjs +170 -0
- package/cli/next.mjs +349 -90
- package/cli/openpr.mjs +191 -39
- package/cli/people.mjs +654 -0
- package/cli/plan.mjs +417 -132
- package/cli/platform.mjs +110 -129
- package/cli/product-index.mjs +287 -0
- package/cli/protection.mjs +706 -0
- package/cli/reconcile.mjs +38 -12
- package/cli/repo-publish.mjs +24 -26
- package/cli/repo.mjs +23 -14
- package/cli/report.mjs +21 -15
- package/cli/review.mjs +24 -27
- package/cli/riskmap-command.mjs +289 -0
- package/cli/riskmap.mjs +373 -0
- package/cli/setup.mjs +139 -287
- package/cli/ship.mjs +7 -6
- package/cli/skill.mjs +180 -0
- package/cli/skip.mjs +211 -30
- package/cli/thread.mjs +42 -17
- package/cli/tidy.mjs +20 -20
- package/cli/update-commit.mjs +22 -22
- package/cli/usage.mjs +115 -109
- package/package.json +3 -3
- package/skills/sdlc/config.yaml +166 -87
- package/skills/sdlc/module-help.csv +35 -35
- package/skills/yad-analysis/SKILL.md +125 -65
- package/skills/yad-architecture/SKILL.md +34 -23
- package/skills/yad-architecture/references/contract-format.md +10 -8
- package/skills/yad-backfill/SKILL.md +14 -8
- package/skills/yad-backfill/references/backfill.md +1 -1
- package/skills/yad-change/SKILL.md +127 -52
- package/skills/yad-change/references/triage.md +42 -28
- package/skills/yad-checks/SKILL.md +89 -45
- package/skills/yad-checks/references/check-gates.md +315 -92
- package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
- package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
- package/skills/yad-checks/templates/checks/contract-check.sh +58 -2
- package/skills/yad-checks/templates/checks/epic-open.sh +3 -3
- package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
- package/skills/yad-checks/templates/checks/ledger-guard.sh +94 -18
- package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
- package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +4 -4
- package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
- package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
- package/skills/yad-checks/templates/github/yad-checks.yml +37 -5
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
- package/skills/yad-checks/templates/github/yad-update-guard.yml +3 -4
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
- package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +22 -4
- package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +4 -4
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +38 -7
- package/skills/yad-commit/SKILL.md +6 -6
- package/skills/yad-connect-design/SKILL.md +6 -6
- package/skills/yad-connect-design/references/design-context.md +1 -1
- package/skills/yad-connect-design/references/design-registry.md +2 -2
- package/skills/yad-connect-docs/SKILL.md +12 -12
- package/skills/yad-connect-docs/references/docs-registry.md +1 -1
- package/skills/yad-connect-learning/SKILL.md +5 -5
- package/skills/yad-connect-learning/references/learning-registry.md +2 -2
- package/skills/yad-connect-repos/SKILL.md +92 -54
- package/skills/yad-connect-repos/references/code-context.md +6 -6
- package/skills/yad-connect-repos/references/hub-config.md +68 -58
- package/skills/yad-connect-repos/references/repos-registry.md +10 -9
- package/skills/yad-connect-repos/references/risk-map.md +81 -0
- package/skills/yad-connect-testing/SKILL.md +6 -6
- package/skills/yad-connect-testing/references/testing-context.md +3 -4
- package/skills/yad-connect-testing/references/testing-registry.md +2 -2
- package/skills/yad-defects/SKILL.md +8 -8
- package/skills/yad-discovery/SKILL.md +130 -94
- package/skills/yad-discovery/references/discovery-schema.md +23 -7
- package/skills/yad-discovery/references/foundation-schema.md +374 -0
- package/skills/yad-docs/SKILL.md +16 -11
- package/skills/yad-docs/references/data-mapping.md +9 -7
- package/skills/yad-docs/templates/app/package-lock.json +3 -3
- package/skills/yad-docs-overview/SKILL.md +32 -17
- package/skills/yad-docs-overview/references/pipeline-model.md +47 -28
- package/skills/yad-docs-sync/SKILL.md +10 -5
- package/skills/yad-docs-sync/references/staleness.md +8 -7
- package/skills/yad-engineer-review/SKILL.md +88 -24
- package/skills/yad-engineer-review/references/ship-and-record.md +25 -16
- package/skills/yad-epic/SKILL.md +178 -100
- package/skills/yad-epic/references/state-schema.md +626 -117
- package/skills/yad-hub-bridge/SKILL.md +66 -48
- package/skills/yad-hub-bridge/references/bridge.md +110 -83
- package/skills/yad-hub-bridge/references/login-roster.md +163 -70
- package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
- package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
- package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
- package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +22 -12
- package/skills/yad-implement/SKILL.md +29 -15
- package/skills/yad-implement/references/implement-conventions.md +2 -2
- package/skills/yad-learn/SKILL.md +9 -9
- package/skills/yad-learn/references/learning-state.md +2 -2
- package/skills/yad-open-pr/SKILL.md +64 -29
- package/skills/yad-pair-review/SKILL.md +18 -16
- package/skills/yad-pair-review/references/session-state.md +4 -4
- package/skills/yad-pr-template/SKILL.md +48 -27
- package/skills/yad-pr-template/references/risk-routing.md +97 -24
- package/skills/yad-pr-template/templates/checks/pr-template.sh +37 -15
- package/skills/yad-pr-template/templates/checks/pr-title.sh +27 -13
- package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
- package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
- package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
- package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +15 -14
- package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +15 -13
- package/skills/yad-reconcile/SKILL.md +3 -3
- package/skills/yad-report/SKILL.md +5 -5
- package/skills/yad-review-companion/SKILL.md +12 -9
- package/skills/yad-review-gate/SKILL.md +198 -79
- package/skills/yad-review-gate/references/gating.md +230 -54
- package/skills/yad-run/SKILL.md +86 -56
- package/skills/yad-run/references/run-loop.md +67 -45
- package/skills/yad-ship/SKILL.md +18 -14
- package/skills/yad-spec/SKILL.md +31 -17
- package/skills/yad-spec/references/spec-handoff.md +17 -5
- package/skills/yad-status/SKILL.md +114 -56
- package/skills/yad-stories/SKILL.md +42 -27
- package/skills/yad-stories/references/story-schema.md +10 -9
- package/skills/yad-stub/SKILL.md +59 -48
- package/skills/yad-sync-repos/SKILL.md +3 -3
- package/skills/yad-test-cases/SKILL.md +37 -30
- package/skills/yad-test-cases/references/test-cases-schema.md +8 -5
- package/skills/yad-timeline/SKILL.md +8 -7
- package/skills/yad-ui/SKILL.md +46 -25
- package/cli/roster.mjs +0 -164
- package/skills/sdlc/install.sh +0 -68
|
@@ -1,70 +1,163 @@
|
|
|
1
|
-
# Login
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
# Login attribution — who a record names
|
|
2
|
+
|
|
3
|
+
This file used to describe the reviewer roster; E62 removed the roster, and it now describes how records
|
|
4
|
+
are attributed without one.
|
|
5
|
+
|
|
6
|
+
yadflow keeps **no stored list of people**. The platform already knows who reviewed, and repository
|
|
7
|
+
access already decides who may approve. So every record names the **platform login** that acted.
|
|
8
|
+
|
|
9
|
+
## Approvals and comments
|
|
10
|
+
|
|
11
|
+
When `yad gate sync` (or CI, `yad gate ci`) reads a review PR/MR:
|
|
12
|
+
|
|
13
|
+
- An approval becomes one `approvals.json` record per person:
|
|
14
|
+
```json
|
|
15
|
+
{ "artifact": "<artifact>", "step": "<step id>", "approver": "<platform login>", "status": "approved",
|
|
16
|
+
"date": "<YYYY-MM-DD>", "source": "bridge", "artifactHash": "<hash>", "approvedAt": "<time>",
|
|
17
|
+
"pr": <n>, "commit": "<sha>", "url": "<review url>", "reviewId": "<id>", "engagement": "verified|none" }
|
|
18
|
+
```
|
|
19
|
+
`pr` is present when the PR/MR number is known. There is no `role`, no `domain` and no `unverified`
|
|
20
|
+
flag. `commit`, `url` and `reviewId` are the platform's evidence for the review (E64), written only
|
|
21
|
+
when the platform gives them — GitHub gives all three; GitLab gives none of them, because an MR
|
|
22
|
+
approval is not tied to a commit. They are a record of what happened; the gate still decides on the
|
|
23
|
+
login and the fingerprint. `approvedAt` is the time the platform gives: GitHub's submission time, or
|
|
24
|
+
GitLab's `approved_at`. A GitLab instance that does not send `approved_at`, and every GitLab record
|
|
25
|
+
written before E64, hold the date the sync first recorded the approval instead. A date is read as
|
|
26
|
+
"time unknown", never compared with a time.
|
|
27
|
+
- A comment becomes a `comments.json` record:
|
|
28
|
+
`{ artifact, step, commenter: <platform login>, round, count, date }` — no `role`.
|
|
29
|
+
- The dated side file `reviews/<artifact>--<date>--approved.md` lists
|
|
30
|
+
`- <approver> — approved <date> (<source>)`. An older record's role is printed only if that record
|
|
31
|
+
still has one.
|
|
32
|
+
|
|
33
|
+
There is no lookup step: the login the platform reports is the name written. The gate counts distinct
|
|
34
|
+
`approver` values, so the same person is one approver however many records name them.
|
|
35
|
+
|
|
36
|
+
**Local ledger (no platform).** The `yad-review-gate` skill writes the same shapes by hand. `approver` /
|
|
37
|
+
`commenter` is the reviewer's platform login, or the name they give when there is no platform. A team
|
|
38
|
+
gate needs 1 distinct approver (solo mode waives it); nothing checks that it is not the artifact's author, so do not record the
|
|
39
|
+
author's own approval.
|
|
40
|
+
|
|
41
|
+
## Older records
|
|
42
|
+
|
|
43
|
+
Records written while the roster existed may carry `role` and `domain`, and may name a person by their
|
|
44
|
+
roster `name` instead of their login. The gate never counts a role; the records stay on disk.
|
|
45
|
+
|
|
46
|
+
On the first sync after the upgrade, `yad gate sync` recognises the person such a bridge record names,
|
|
47
|
+
so it continues that record instead of treating the approval as new:
|
|
48
|
+
0. **An exact submission time comes first (GitHub).** When exactly one older record holds the second a
|
|
49
|
+
review was submitted, that review continues it, whatever name the roster now gives it. It continues the
|
|
50
|
+
records **of that review only** — the ones holding that second — never the whole name: two people an
|
|
51
|
+
older release wrote under one name each keep their own record, and neither reads as the other's newer
|
|
52
|
+
review. A name already continued by a time is not an "orphan" for a later approval to guess at (step 2).
|
|
53
|
+
A match by name is per review too: when some of the name's records hold the approval's own time, it
|
|
54
|
+
continues those. On a closed step, a record under that name that no review continued is kept as
|
|
55
|
+
history. **Known cost (chosen 2026-09-17):** the records cannot tell two people who once shared a name
|
|
56
|
+
from one person whose role records ended up at two different times (older releases moved all of a
|
|
57
|
+
person's records together, so that needs a role dropped on a closed step and a later re-approval, or a
|
|
58
|
+
hand edit). Keeping protects the first, likelier case; in the second, that closed step lists the person
|
|
59
|
+
twice, so `gate status` shows one approver more. Nothing advances on a closed step. The name table
|
|
60
|
+
can be wrong — renaming one of two people who shared a name hands the older records to whoever kept
|
|
61
|
+
it. Older GitLab records hold only a date, so there a rename can still put an older approval on the
|
|
62
|
+
wrong person: **do not rename a shared roster name while reviews with older approvals are open.**
|
|
63
|
+
1. **With the roster still on disk** — the roster's `name` → `login` pairs make the match exact. This is
|
|
64
|
+
the ONLY thing the roster is read for; it decides nothing about the gate. A name equal to another
|
|
65
|
+
entry's login is still exact (the older record under a name was written from that entry). The one
|
|
66
|
+
exception is a name the roster gives to TWO logins: a record under it could be either person, so it is
|
|
67
|
+
treated like a record no name can place (step 2), and `yad doctor` warns `people:roster-ambiguous`. A
|
|
68
|
+
record an older release marked `unverified` already names a login and is never translated.
|
|
69
|
+
2. **Without a roster** — a record is continued only when nothing else could be it: the same submission
|
|
70
|
+
time (GitHub), or — on an open step only — exactly one unmatched approval against exactly one
|
|
71
|
+
unmatched older approver of that PR (GitLab, whose older records hold only a date — this rule still
|
|
72
|
+
applies when the approval now carries `approved_at`). People are never matched
|
|
73
|
+
by list order, and the result is the same whatever order the platform lists reviews in.
|
|
74
|
+
3. The continued record keeps the fingerprint and dates it carried, unless the platform shows a newer
|
|
75
|
+
review (a later submission time — a time on both sides, so a date-only record never reads as older —
|
|
76
|
+
or a different PR/MR). When one person's older records
|
|
77
|
+
disagree, the **stale** one is kept — stale meaning outside the fingerprints the gate accepts for the
|
|
78
|
+
artifact (`acceptedHashes`), not merely different from today's hash.
|
|
79
|
+
A continued record is rewritten under the login and drops `role`, `domain` and `unverified`: they
|
|
80
|
+
count for nothing, and a `role` left on a login-named record would make the next sync read it as an
|
|
81
|
+
older record and look its login up as a roster name — which can be another person's. The dated
|
|
82
|
+
`reviews/<artifact>--<date>--approved.md` keeps the roles as they were.
|
|
83
|
+
4. **Still ambiguous:** on a closed step nothing is added (its record is history, and a second entry would
|
|
84
|
+
count one person twice); on an open step the approval is recorded against a stale fingerprint from
|
|
85
|
+
those older records, so it must be given again on a new review. If none of those older records is
|
|
86
|
+
stale, there is nothing stale to bind to, and the approval is recorded against the current content.
|
|
87
|
+
|
|
88
|
+
Comment rounds use the same name → login pairs, so the first sync does not open a new round for
|
|
89
|
+
unchanged threads. Without the roster (or for a name two logins share) that sync may open one new round.
|
|
90
|
+
|
|
91
|
+
## Recording the login on older records (E64)
|
|
92
|
+
|
|
93
|
+
The steps above need the roster. So, while it is still on disk, every sync write (`yad gate sync`,
|
|
94
|
+
`yad gate ci`) records the login on each older record the roster can place for certain. After that, only
|
|
95
|
+
the records it could not place (below) still need the roster.
|
|
96
|
+
|
|
97
|
+
- **What changes on a record.** `approver` / `commenter` becomes the login, and `role`, `domain` and
|
|
98
|
+
`unverified` are removed, exactly as step 3 rewrites a continued record. An approval keeps the name it
|
|
99
|
+
had in `rosterName`. Everything else — fingerprint, dates, `pr` — stays as it was. The dated
|
|
100
|
+
`reviews/*--approved.md` keeps the roles.
|
|
101
|
+
- **One record per person, but only for one review.** An older release wrote one record per role, so one
|
|
102
|
+
approval can be several records. They become one record — keeping the stale fingerprint when they
|
|
103
|
+
disagree — only when they prove they are one review: every one holds the same submission **time** and
|
|
104
|
+
the same `pr`. A date is not proof. Every GitLab record from before E64 holds the day it was synced, so
|
|
105
|
+
two people a renamed roster now gives one name look the same; and a hand-written record holds no time
|
|
106
|
+
at all. Such records are left as they are. On GitLab this means one person's role records usually stay
|
|
107
|
+
as they are, and the roster keeps matching them on the next sync of their review.
|
|
108
|
+
- **Also left as they are:** a record marked `unverified` (it names a login already), a record under a
|
|
109
|
+
name two logins share, a name the roster does not hold, and a comment round in which two records would
|
|
110
|
+
then name the same login.
|
|
111
|
+
- **The exact time still wins.** An approval stamped onto the wrong login (a renamed shared name) keeps
|
|
112
|
+
`rosterName`, so step 0 still matches it by its submission time and hands it to the right person.
|
|
113
|
+
- **When it runs.** `yad gate sync` stamps every step of the epic it writes, closed steps included. On a
|
|
114
|
+
verified Product, `yad gate ci` does it on every merge run — the merge event and the scheduled reconcile
|
|
115
|
+
that re-runs it — for **every** epic, including one with no open review, and commits it with the rest.
|
|
116
|
+
It never writes before a merge; it skips an epic with uncommitted changes under `.sdlc` or `reviews`;
|
|
117
|
+
a run whose sync fails leaves no stamp behind; and a record it cannot read, or an epic whose stamp fails, is left as it
|
|
118
|
+
is and named, in any epic — the merged review's own included — without stopping that merge. `yad gate sync <epic>` writes the stamp even when the epic
|
|
119
|
+
has no review PR on file. A Product with no platform never needs it: nothing there matches logins.
|
|
120
|
+
- **When the roster can go.** `yad doctor`'s `people:roster-unused` says so: how many older records still
|
|
121
|
+
wait for a gate write (and in which epics), or that none do. Records it cannot place (a shared name,
|
|
122
|
+
records that disagree about the review) are then matched by submission time or given again on a new
|
|
123
|
+
review.
|
|
124
|
+
|
|
125
|
+
## Who wrote a record (`by`)
|
|
126
|
+
|
|
127
|
+
Skip, defer, closing records, kill-switch flips and mode changes carry a `by`. It is:
|
|
128
|
+
|
|
129
|
+
1. the login `gh api user` (GitHub, asked on the repo's own host) or `glab api user` (GitLab) reports, else
|
|
130
|
+
2. git `user.name`, else `null`.
|
|
131
|
+
|
|
132
|
+
`YAD_PLATFORM_LOGIN=0` turns the platform lookup off (offline work, tests). The lookup is best effort and
|
|
133
|
+
never blocks the command. Checkpoint and tidy commit subjects show `@<login>` when the login is known,
|
|
134
|
+
else the git name.
|
|
135
|
+
|
|
136
|
+
## Opening a PR/MR
|
|
137
|
+
|
|
138
|
+
`yad gate open` (review PRs) and `yad open-pr` (task PRs):
|
|
139
|
+
|
|
140
|
+
- **Assignee** = the person opening it: `@me` on GitHub (`gh` resolves it), the login `glab api user`
|
|
141
|
+
reports on GitLab (no assignee when that lookup fails).
|
|
142
|
+
- **Reviewers: none are requested.** The command prints
|
|
143
|
+
`no reviewers were requested — ask them on the PR itself`. On a code-repo PR, `yad open-pr` then
|
|
144
|
+
prints a reviewer suggestion from recent history and CODEOWNERS (E68) — a hint, never a request.
|
|
145
|
+
- **Labels (`yad gate open` only):** a `domain:<repo>` label per touched domain is still applied — for
|
|
146
|
+
`stories-review` the union of every story's `repos`, for a step tagged `contract`, `auth` or
|
|
147
|
+
`payments` the epic's `repos`. The labels are a hint for whom to ask; they add no approvals.
|
|
148
|
+
`yad open-pr` adds no labels.
|
|
149
|
+
|
|
150
|
+
## Legacy data `yad doctor` reports
|
|
151
|
+
|
|
152
|
+
None of these decides anything any more. `yad doctor` warns so the team can delete them:
|
|
153
|
+
|
|
154
|
+
| Warning | What is left on disk |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `people:roster-unused` | a non-empty `roster` in `.sdlc/hub.json` — still read for its name → login pairs only (see "Older records"); the hint counts the older records still waiting for a gate write to record their login, and says when it is safe to delete |
|
|
157
|
+
| `people:roster-ambiguous` | a roster name given to more than one login (only checked when the roster is non-empty) |
|
|
158
|
+
| `people:domain-owners-unused` | a repo in `.sdlc/repos.json` that lists `domain_owner` / `domain_owners` |
|
|
159
|
+
| `people:verified-authors-unused` | a `verified_authors` list in `.sdlc/hub.json`, or a `.sdlc/verified-authors` file in the Product or a connected repo |
|
|
160
|
+
| `people:allowlist-gate-stale` | an older `checks/verified-commits.sh` that still enforces the author list (a local-ledger Product's CI files are not refreshed by `yad check --fix`) |
|
|
161
|
+
|
|
162
|
+
`yad roster` is removed (typing it prints a notice and exits 1), and `yad setup` collects no reviewers,
|
|
163
|
+
roles, repo owners or commit emails. The verified-commits gate checks platform-Verified signatures only.
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# PR/MR description's "Impact & Risk (front-half)" block and prints the
|
|
4
|
-
#
|
|
5
|
-
# the
|
|
2
|
+
# Product review routing — the Shape analogue of yad-pr-template's risk-route.sh. Reads a Product review
|
|
3
|
+
# PR/MR description's "Impact & Risk (front-half)" block and prints how many approvers the gate asks for,
|
|
4
|
+
# as the same sum the gate prints: base 1, plus a risk step from the tags — `contract` +2, `auth` or
|
|
5
|
+
# `payments` +1, the largest and never the sum. Only the base holds the gate; the risk step is advisory.
|
|
6
|
+
# The gate reports that count capped at the number of active people less one (E72); this
|
|
7
|
+
# script cannot count people, so it prints the full sum. There are no roles and no named owners: yadflow
|
|
8
|
+
# keeps no list of people (E62), so when a risk tag raises the count the touched repos are printed as a
|
|
9
|
+
# hint for whom to ask (with no risk step it prints the base line alone). Advisory: it ROUTES the human
|
|
6
10
|
# review; it does not approve or merge.
|
|
7
11
|
set -euo pipefail
|
|
8
12
|
|
|
@@ -18,33 +22,32 @@ value_of() {
|
|
|
18
22
|
|
|
19
23
|
risk_tags="$(printf '%s' "$(value_of 'Risk tags:')" | tr 'A-Z' 'a-z')"
|
|
20
24
|
repos="$(value_of 'Domains.*touched:')"
|
|
21
|
-
artifact="$(value_of 'Artifact:')"
|
|
22
25
|
|
|
23
26
|
echo "Risk tags: ${risk_tags:-none}"
|
|
24
27
|
echo "Repos touched: ${repos:-unspecified}"
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
step=0
|
|
30
|
+
tier=""
|
|
27
31
|
why=""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
esac
|
|
31
|
-
case "$
|
|
32
|
-
case "$
|
|
33
|
-
# The stories review routes per-repo even with no risk tag.
|
|
34
|
-
case "$artifact" in *stories*) escalate=yes; why="${why:+$why, }stories per-repo routing" ;; esac
|
|
32
|
+
# Whole tags only, as the gate reads them (`gateRuleFor`): `oauth` or `contracts` is not a risk tag.
|
|
33
|
+
tags=" $(printf '%s' "$risk_tags" | tr ',;' ' ' | tr -s '[:space:]' ' ') "
|
|
34
|
+
case "$tags" in *" auth "*) step=1; tier=high; why="risk tag: auth" ;; esac
|
|
35
|
+
case "$tags" in *" payments "*) step=1; tier=high; why="${why:+$why, }risk tag: payments" ;; esac
|
|
36
|
+
case "$tags" in *" contract "*) step=2; tier=contract; why="${why:+$why, }risk tag: contract" ;; esac
|
|
35
37
|
|
|
36
|
-
if [ "$
|
|
37
|
-
echo "ROUTE:
|
|
38
|
-
echo "
|
|
38
|
+
if [ "$step" -gt 0 ]; then
|
|
39
|
+
echo "ROUTE: $((1 + step)) approvers = base 1 + ${tier} risk ${step} (${why})"
|
|
40
|
+
echo " Only the base holds the gate: 1 approval (the platform decides whether the author may give it)."
|
|
41
|
+
echo " The risk step is advisory; \`yad gate status\` prints it capped by the active people. Ask reviewers who know the touched repos:"
|
|
39
42
|
case "$repos" in
|
|
40
43
|
""|*"<"*|*"…"*|*"|"*)
|
|
41
|
-
echo " (Repos line not filled in — list each touched repo
|
|
44
|
+
echo " (Repos line not filled in — list each touched repo so the right reviewers can be asked.)" ;;
|
|
42
45
|
*)
|
|
43
46
|
printf '%s\n' "$repos" | tr ',' '\n' | while IFS= read -r r; do
|
|
44
47
|
r="$(printf '%s' "$r" | sed -E 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
45
|
-
[ -n "$r" ]
|
|
48
|
+
if [ -n "$r" ]; then echo " - $r"; fi # not `&&`: an empty last item (`a, b,`) would exit 1
|
|
46
49
|
done ;;
|
|
47
50
|
esac
|
|
48
51
|
else
|
|
49
|
-
echo "ROUTE:
|
|
52
|
+
echo "ROUTE: 1 approver = base 1 (no risk step)."
|
|
50
53
|
fi
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# yad-managed: yad-hub-bridge
|
|
2
|
-
# Merge-time gate sync for the PRODUCT
|
|
2
|
+
# Merge-time gate sync for the PRODUCT (Path B). CI is the SOLE writer of the ledger, and it
|
|
3
3
|
# writes ONLY at merge, ONLY to the default branch.
|
|
4
4
|
#
|
|
5
5
|
# During review there is NO CI write: the platform PR is the source of truth (native approvals +
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
# last part is load-bearing: before the #163 fix the re-sync re-appended each step's approvals at the
|
|
25
25
|
# tail, so this job rotated approvals.json and committed the reorder every 15 minutes, forever (issue
|
|
26
26
|
# #163). These jobs run an EXACT version resolved from the repo (see the resolver in each job), so a
|
|
27
|
-
#
|
|
27
|
+
# Product sitting on a pre-#163 yadflow keeps that behaviour until someone upgrades it — if the resolved
|
|
28
28
|
# pin is older than 3.15.3, run `yad update` or disable the schedule.
|
|
29
29
|
# On GitHub a scheduled workflow runs automatically once committed (no setup).
|
|
30
30
|
#
|
|
@@ -48,13 +48,13 @@ permissions:
|
|
|
48
48
|
|
|
49
49
|
env:
|
|
50
50
|
# The manual override for which yadflow these jobs run: set a repository (or organization) Actions
|
|
51
|
-
# **variable** named YAD_VERSION to e.g. `
|
|
51
|
+
# **variable** named YAD_VERSION to e.g. `4.0.0`, under Settings → Secrets and variables → Actions →
|
|
52
52
|
# Variables. It lives there and not in this file, which `yad` owns and rewrites byte-for-byte from
|
|
53
53
|
# its template on every sync — a version edited in here would be silently reverted.
|
|
54
54
|
#
|
|
55
55
|
# Left EMPTY the jobs resolve an exact pin from the repo itself instead of floating on the major (see
|
|
56
56
|
# the resolver step in each job). Floating meant a release could change what a scheduled job does with
|
|
57
|
-
# nobody deciding to upgrade — the amplification path in issue #163, where 3.13.1 took one
|
|
57
|
+
# nobody deciding to upgrade — the amplification path in issue #163, where 3.13.1 took one Product from 20
|
|
58
58
|
# to 96 churn commits an hour.
|
|
59
59
|
YAD_VERSION: ${{ vars.YAD_VERSION || '' }}
|
|
60
60
|
|
|
@@ -87,9 +87,9 @@ jobs:
|
|
|
87
87
|
# >>> yad-pin
|
|
88
88
|
# Which yadflow this job runs, most explicit source first:
|
|
89
89
|
# 1. $YAD_VERSION — the platform variable, a human's deliberate override, verbatim
|
|
90
|
-
# 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this
|
|
90
|
+
# 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this Product
|
|
91
91
|
# 3. .sdlc/cli-version.json — the version that last wired it (`yad update` re-stamps this)
|
|
92
|
-
# 4.
|
|
92
|
+
# 4. $YAD_MAJOR — floating major, only when nothing above resolves
|
|
93
93
|
# Sources 2 and 3 are machine-written files a stale or hand-edited checkout can get wrong, so
|
|
94
94
|
# they are VALIDATED: an exact release token of the major this fragment was written for. That
|
|
95
95
|
# also keeps the value safe to interpolate into `npx -p "yadflow@$V"` — a repo-controlled
|
|
@@ -98,20 +98,30 @@ jobs:
|
|
|
98
98
|
# Pipes, not bash here-strings, so the block stays byte-identical to the GitLab fragment's
|
|
99
99
|
# (which must run under `sh` when an image has no bash). `sed` drains its input, so nothing
|
|
100
100
|
# SIGPIPEs `tr`.
|
|
101
|
+
# The major this fragment ships with. ONE literal, read by the check, the message and the fallback
|
|
102
|
+
# below, so they cannot drift apart; scripts/pin-major-check.sh holds it to the major a release
|
|
103
|
+
# publishes. `yad update` rewrites this file and re-stamps .sdlc/cli-version.json together, so a
|
|
104
|
+
# Product moves both to the new major at once — and a pin from an older major is skipped.
|
|
105
|
+
YAD_MAJOR=4
|
|
101
106
|
yad_pin_read() { # <file> <key>
|
|
102
107
|
[ -f "$1" ] || return 0
|
|
103
108
|
tr -d '\n' < "$1" 2>/dev/null | sed -nE "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/p"
|
|
104
109
|
}
|
|
105
|
-
yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq
|
|
110
|
+
yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq "^${YAD_MAJOR}\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?\$"; }
|
|
106
111
|
YAD_PKG="${YAD_VERSION:-}"
|
|
107
112
|
if [ -z "$YAD_PKG" ]; then
|
|
108
113
|
for _cand in "$(yad_pin_read .sdlc/hub.json gate_sync_version)" "$(yad_pin_read .sdlc/cli-version.json version)"; do
|
|
109
114
|
[ -n "$_cand" ] || continue
|
|
110
115
|
if yad_pin_valid "$_cand"; then YAD_PKG="$_cand"; break; fi
|
|
111
|
-
echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact
|
|
116
|
+
echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact ${YAD_MAJOR}.x release." >&2
|
|
112
117
|
done
|
|
113
118
|
fi
|
|
114
|
-
|
|
119
|
+
if [ -z "$YAD_PKG" ]; then
|
|
120
|
+
YAD_PKG="$YAD_MAJOR"
|
|
121
|
+
# A bare major is an npm RANGE, and a range never matches a prerelease: while only ${YAD_MAJOR}.0.0-next.N
|
|
122
|
+
# exists, npm fails with ETARGET. Say what to do before that happens.
|
|
123
|
+
echo "yad-gate-sync: no exact pin resolved — floating on yadflow@${YAD_MAJOR}. If npm cannot find it, run: yad update — and commit .sdlc/cli-version.json — or set YAD_VERSION." >&2
|
|
124
|
+
fi
|
|
115
125
|
echo "yad-gate-sync: running yadflow@${YAD_PKG}"
|
|
116
126
|
# <<< yad-pin
|
|
117
127
|
echo "YAD_PKG=${YAD_PKG}" >> "$GITHUB_ENV"
|
|
@@ -155,9 +165,9 @@ jobs:
|
|
|
155
165
|
# >>> yad-pin
|
|
156
166
|
# Which yadflow this job runs, most explicit source first:
|
|
157
167
|
# 1. $YAD_VERSION — the platform variable, a human's deliberate override, verbatim
|
|
158
|
-
# 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this
|
|
168
|
+
# 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this Product
|
|
159
169
|
# 3. .sdlc/cli-version.json — the version that last wired it (`yad update` re-stamps this)
|
|
160
|
-
# 4.
|
|
170
|
+
# 4. $YAD_MAJOR — floating major, only when nothing above resolves
|
|
161
171
|
# Sources 2 and 3 are machine-written files a stale or hand-edited checkout can get wrong, so
|
|
162
172
|
# they are VALIDATED: an exact release token of the major this fragment was written for. That
|
|
163
173
|
# also keeps the value safe to interpolate into `npx -p "yadflow@$V"` — a repo-controlled
|
|
@@ -166,20 +176,30 @@ jobs:
|
|
|
166
176
|
# Pipes, not bash here-strings, so the block stays byte-identical to the GitLab fragment's
|
|
167
177
|
# (which must run under `sh` when an image has no bash). `sed` drains its input, so nothing
|
|
168
178
|
# SIGPIPEs `tr`.
|
|
179
|
+
# The major this fragment ships with. ONE literal, read by the check, the message and the fallback
|
|
180
|
+
# below, so they cannot drift apart; scripts/pin-major-check.sh holds it to the major a release
|
|
181
|
+
# publishes. `yad update` rewrites this file and re-stamps .sdlc/cli-version.json together, so a
|
|
182
|
+
# Product moves both to the new major at once — and a pin from an older major is skipped.
|
|
183
|
+
YAD_MAJOR=4
|
|
169
184
|
yad_pin_read() { # <file> <key>
|
|
170
185
|
[ -f "$1" ] || return 0
|
|
171
186
|
tr -d '\n' < "$1" 2>/dev/null | sed -nE "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/p"
|
|
172
187
|
}
|
|
173
|
-
yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq
|
|
188
|
+
yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq "^${YAD_MAJOR}\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?\$"; }
|
|
174
189
|
YAD_PKG="${YAD_VERSION:-}"
|
|
175
190
|
if [ -z "$YAD_PKG" ]; then
|
|
176
191
|
for _cand in "$(yad_pin_read .sdlc/hub.json gate_sync_version)" "$(yad_pin_read .sdlc/cli-version.json version)"; do
|
|
177
192
|
[ -n "$_cand" ] || continue
|
|
178
193
|
if yad_pin_valid "$_cand"; then YAD_PKG="$_cand"; break; fi
|
|
179
|
-
echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact
|
|
194
|
+
echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact ${YAD_MAJOR}.x release." >&2
|
|
180
195
|
done
|
|
181
196
|
fi
|
|
182
|
-
|
|
197
|
+
if [ -z "$YAD_PKG" ]; then
|
|
198
|
+
YAD_PKG="$YAD_MAJOR"
|
|
199
|
+
# A bare major is an npm RANGE, and a range never matches a prerelease: while only ${YAD_MAJOR}.0.0-next.N
|
|
200
|
+
# exists, npm fails with ETARGET. Say what to do before that happens.
|
|
201
|
+
echo "yad-gate-sync: no exact pin resolved — floating on yadflow@${YAD_MAJOR}. If npm cannot find it, run: yad update — and commit .sdlc/cli-version.json — or set YAD_VERSION." >&2
|
|
202
|
+
fi
|
|
183
203
|
echo "yad-gate-sync: running yadflow@${YAD_PKG}"
|
|
184
204
|
# <<< yad-pin
|
|
185
205
|
echo "YAD_PKG=${YAD_PKG}" >> "$GITHUB_ENV"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# SDLC gate sync — minimal root pipeline for the PRODUCT
|
|
2
|
-
# Written by the yad-hub-bridge `wire` action ONLY when the
|
|
1
|
+
# SDLC gate sync — minimal root pipeline for the PRODUCT.
|
|
2
|
+
# Written by the yad-hub-bridge `wire` action ONLY when the Product has no existing root
|
|
3
3
|
# `.gitlab-ci.yml`. The job itself lives in the included fragment so the same single source is
|
|
4
4
|
# reused whether or not a root pipeline already exists. If a root later grows real jobs, they
|
|
5
5
|
# coexist with the included yad-* job (which carries `needs: []` and no `stage:`).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# yad-managed-include: yad-hub-bridge
|
|
2
|
-
# Merge-time gate sync for the PRODUCT
|
|
3
|
-
#
|
|
2
|
+
# Merge-time gate sync for the PRODUCT, as an INCLUDABLE fragment (Path B). Pulled into the
|
|
3
|
+
# Product's root .gitlab-ci.yml via:
|
|
4
4
|
# include:
|
|
5
5
|
# - local: '.gitlab/ci/yad-gate-sync.yml'
|
|
6
6
|
# so wiring never edits the foreign root pipeline beyond that one include line. The job carries
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
# default branch — the merge advance pushes there) and store it as a masked CI/CD variable
|
|
35
35
|
# SDLC_GATE_TOKEN. Without it the job fails visibly; recover by setting the token, then re-run the
|
|
36
36
|
# pipeline or run `yad gate ci --branch <review-branch> --pr <iid> --merged` locally on the default
|
|
37
|
-
# branch (advisory `yad gate sync` is read-only in
|
|
37
|
+
# branch (advisory `yad gate sync` is read-only in verified mode and cannot recover a stuck gate).
|
|
38
38
|
#
|
|
39
39
|
# Which yadflow this job runs: an EXACT version resolved from the repo at run time — see the `yad-pin`
|
|
40
40
|
# block in the script below for the precedence. It no longer floats on the major, because floating let a
|
|
41
41
|
# release change what a scheduled job does with nobody deciding to upgrade (issue #163, where 3.13.1
|
|
42
|
-
# took one
|
|
43
|
-
#
|
|
42
|
+
# took one Product from 20 to 96 churn commits an hour). The trade-off is deliberate and cuts both ways: a
|
|
43
|
+
# Product is no longer carried onto a fix automatically, so if the resolved pin is older than 3.15.3, run
|
|
44
44
|
# `yad update` or disable this schedule.
|
|
45
45
|
#
|
|
46
|
-
# To override it, set a CI/CD variable YAD_VERSION (e.g. `
|
|
46
|
+
# To override it, set a CI/CD variable YAD_VERSION (e.g. `4.0.0`) in project Settings → CI/CD →
|
|
47
47
|
# Variables — the same place SDLC_GATE_TOKEN lives, and deliberately NOT in this file, which `yad` owns
|
|
48
48
|
# and rewrites byte-for-byte from its template on every sync.
|
|
49
49
|
variables:
|
|
@@ -90,9 +90,9 @@ yad-gate-sync:
|
|
|
90
90
|
# >>> yad-pin
|
|
91
91
|
# Which yadflow this job runs, most explicit source first:
|
|
92
92
|
# 1. $YAD_VERSION — the CI/CD variable, a human's deliberate override, verbatim
|
|
93
|
-
# 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this
|
|
93
|
+
# 2. .sdlc/hub.json — "gate_sync_version", the committed pin for this Product
|
|
94
94
|
# 3. .sdlc/cli-version.json — the version that last wired it (`yad update` re-stamps this)
|
|
95
|
-
# 4.
|
|
95
|
+
# 4. $YAD_MAJOR — floating major, only when nothing above resolves
|
|
96
96
|
# Sources 2 and 3 are machine-written files a stale or hand-edited checkout can get wrong, so
|
|
97
97
|
# they are VALIDATED: an exact release token of the major this fragment was written for. That
|
|
98
98
|
# also keeps the value safe to interpolate into `npx -p "yadflow@$V"` — a repo-controlled
|
|
@@ -101,20 +101,30 @@ yad-gate-sync:
|
|
|
101
101
|
# the same shell, so YAD_PKG carries into the block below.
|
|
102
102
|
# Pipes, not bash here-strings: a GitLab runner falls back to `sh` when the image has no bash,
|
|
103
103
|
# and `<<<` is a bashism. `sed` drains its input (no early exit, so nothing SIGPIPEs `tr`).
|
|
104
|
+
# The major this fragment ships with. ONE literal, read by the check, the message and the fallback
|
|
105
|
+
# below, so they cannot drift apart; scripts/pin-major-check.sh holds it to the major a release
|
|
106
|
+
# publishes. `yad update` rewrites this file and re-stamps .sdlc/cli-version.json together, so a
|
|
107
|
+
# Product moves both to the new major at once — and a pin from an older major is skipped.
|
|
108
|
+
YAD_MAJOR=4
|
|
104
109
|
yad_pin_read() { # <file> <key>
|
|
105
110
|
[ -f "$1" ] || return 0
|
|
106
111
|
tr -d '\n' < "$1" 2>/dev/null | sed -nE "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/p"
|
|
107
112
|
}
|
|
108
|
-
yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq
|
|
113
|
+
yad_pin_valid() { printf '%s' "${1:-}" | grep -Eq "^${YAD_MAJOR}\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?\$"; }
|
|
109
114
|
YAD_PKG="${YAD_VERSION:-}"
|
|
110
115
|
if [ -z "$YAD_PKG" ]; then
|
|
111
116
|
for _cand in "$(yad_pin_read .sdlc/hub.json gate_sync_version)" "$(yad_pin_read .sdlc/cli-version.json version)"; do
|
|
112
117
|
[ -n "$_cand" ] || continue
|
|
113
118
|
if yad_pin_valid "$_cand"; then YAD_PKG="$_cand"; break; fi
|
|
114
|
-
echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact
|
|
119
|
+
echo "yad-gate-sync: ignoring pin '${_cand}' — not an exact ${YAD_MAJOR}.x release." >&2
|
|
115
120
|
done
|
|
116
121
|
fi
|
|
117
|
-
|
|
122
|
+
if [ -z "$YAD_PKG" ]; then
|
|
123
|
+
YAD_PKG="$YAD_MAJOR"
|
|
124
|
+
# A bare major is an npm RANGE, and a range never matches a prerelease: while only ${YAD_MAJOR}.0.0-next.N
|
|
125
|
+
# exists, npm fails with ETARGET. Say what to do before that happens.
|
|
126
|
+
echo "yad-gate-sync: no exact pin resolved — floating on yadflow@${YAD_MAJOR}. If npm cannot find it, run: yad update — and commit .sdlc/cli-version.json — or set YAD_VERSION." >&2
|
|
127
|
+
fi
|
|
118
128
|
echo "yad-gate-sync: running yadflow@${YAD_PKG}"
|
|
119
129
|
# <<< yad-pin
|
|
120
130
|
- |
|
|
@@ -126,7 +136,7 @@ yad-gate-sync:
|
|
|
126
136
|
# unchanged approval record produces an unchanged file and nothing is committed. That last part
|
|
127
137
|
# is load-bearing — before the #163 fix the re-sync re-appended each step's approvals at the
|
|
128
138
|
# tail, so this loop rotated approvals.json and committed the reorder on every pass, forever
|
|
129
|
-
# (issue #163). This job runs an exact pin resolved from the repo, so a
|
|
139
|
+
# (issue #163). This job runs an exact pin resolved from the repo, so a Product still sitting on a
|
|
130
140
|
# pre-3.15.3 yadflow keeps that behaviour — upgrade it or disable this schedule.
|
|
131
141
|
# A stuck review MR (a squash merge whose commit dropped the branch name, or a failed merge
|
|
132
142
|
# push) is always RECENT, so sweep a generous recent window and PAGINATE it fully (--paginate)
|