yadflow 3.12.2 → 3.13.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 +30 -0
- package/bin/yad.mjs +26 -6
- package/cli/checkpoint.mjs +59 -1
- package/cli/doctor.mjs +113 -1
- package/cli/epic-state.mjs +14 -6
- package/cli/gate.mjs +191 -25
- package/cli/ledger.mjs +26 -0
- package/cli/platform.mjs +80 -0
- package/package.json +3 -3
- package/skills/yad-architecture/SKILL.md +15 -3
- package/skills/yad-architecture/references/contract-format.md +4 -1
- package/skills/yad-checks/references/check-gates.md +18 -1
- package/skills/yad-checks/templates/checks/contract-check.sh +46 -3
- package/skills/yad-checks/templates/checks/epic-open.sh +41 -9
- package/skills/yad-checks/templates/checks/lineage-check.sh +38 -9
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +39 -7
- package/skills/yad-checks/templates/checks/spec-link.sh +17 -5
- package/skills/yad-docs/templates/app/package-lock.json +7 -519
- package/skills/yad-engineer-review/references/ship-and-record.md +20 -0
- package/skills/yad-epic/references/state-schema.md +13 -0
- package/skills/yad-hub-bridge/references/bridge.md +50 -5
- package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +6 -3
- package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +13 -2
- package/skills/yad-review-gate/SKILL.md +13 -3
- package/skills/yad-review-gate/references/gating.md +7 -3
- package/skills/yad-spec/references/spec-handoff.md +12 -2
|
@@ -10,7 +10,7 @@ predicate (`../yad-review-gate/references/gating.md`) runs unchanged. The bridge
|
|
|
10
10
|
|---|---|
|
|
11
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
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 |
|
|
13
|
-
| GitHub review dismissed / GitLab approval revoked | the prior bridge `approved` record for that approver is removed on re-sync (see idempotency) |
|
|
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 bridge carry `"source": "bridge"`; **manual** approvals have no such
|
|
16
16
|
tag and are **never** touched by `sync` — the two coexist.
|
|
@@ -82,10 +82,21 @@ login and requested too — otherwise an escalated step is structurally unsatisf
|
|
|
82
82
|
## Idempotent re-sync
|
|
83
83
|
|
|
84
84
|
- Key bridge approvals on `(step, approver, role, domain)`. On re-sync, **upsert** — do not append a
|
|
85
|
-
duplicate.
|
|
86
|
-
-
|
|
87
|
-
|
|
88
|
-
-
|
|
85
|
+
duplicate.
|
|
86
|
+
- **On an OPEN step**, remove any bridge approval whose platform review was dismissed/revoked: the
|
|
87
|
+
platform is the live source of truth while the review is in flight.
|
|
88
|
+
- **On a step already `done`**, the record is only added to or refreshed in place — an approval the
|
|
89
|
+
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
|
|
91
|
+
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.
|
|
95
|
+
- Update the step's `hub-prs.json` `lastSyncedAt` when the sync **learned something** — every sync on
|
|
96
|
+
an open step, and on a closed one only when the approval record actually changed (a re-opened review
|
|
97
|
+
that was re-approved). An identical re-sync leaves it alone, so the ledger does not churn.
|
|
98
|
+
- Running `sync` twice with no platform change is a no-op on the ledger — byte-identical, including
|
|
99
|
+
`comments.json` and the dated `reviews/*.md` side files.
|
|
89
100
|
|
|
90
101
|
## Contract re-lock invalidates prior platform approvals too
|
|
91
102
|
|
|
@@ -201,6 +212,40 @@ the default branch: `yad gate ci --branch <review-branch> --pr <n> --merged` (th
|
|
|
201
212
|
unlike advisory `yad gate sync`). File-only mode (no platform) keeps `yad gate sync` as the local writer.
|
|
202
213
|
The file ledger is still the source of truth.
|
|
203
214
|
|
|
215
|
+
Because CI records the `hub-prs.json` pointer only at merge, a review the ledger has never seen still
|
|
216
|
+
has to be nameable — otherwise `sync` refuses a PR that is sitting merged on the platform. With no
|
|
217
|
+
recorded pointer, `yad gate sync <epic> <artifact>` resolves the PR/MR from the review branch
|
|
218
|
+
(`review/<epic>/<artifact>`) itself, and `--pr <n>` names it outright:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
yad gate sync EP-x architecture.md --pr 42 # advisory in bridge mode; the writer without the bridge
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
An explicit `--pr` also **overrides** a recorded pointer, since a re-opened review is a new PR the
|
|
225
|
+
ledger has not seen. Before its reviewers are bound to the artifact's hash, the number is confirmed to
|
|
226
|
+
be the PR for that artifact's review branch — a mismatch is refused, and a platform that cannot answer
|
|
227
|
+
warns and proceeds. The bridge rule is unchanged: the resolved pointer is adopted into the ledger only
|
|
228
|
+
on the writer path, so a human never leaves a gate-state file in their working tree for `ledger-guard`
|
|
229
|
+
to reject.
|
|
230
|
+
|
|
231
|
+
**In bridge mode `gate sync` stays advisory even with `--pr`** — it prints the predicate and writes
|
|
232
|
+
nothing, because CI owns the ledger. The recovery that actually writes is the command CI itself runs,
|
|
233
|
+
on the default branch: `yad gate ci --branch <review-branch> --pr <n> --merged`.
|
|
234
|
+
|
|
235
|
+
**Serialization.** Both wired jobs push the ledger to the default branch, and on GitLab the scheduled
|
|
236
|
+
sweep's recent-MR window overlaps whatever the merge-push pipeline is handling — so they are pinned to
|
|
237
|
+
one at a time (`concurrency: yad-gate-mergesync` on GitHub, `resource_group: yad-gate-mergesync` on
|
|
238
|
+
GitLab). Without it both runs produce the same advance, one pushes, and the other rebases onto it and
|
|
239
|
+
lands a duplicate `chore(gate): advance …` commit under a different SHA — unreviewed churn, since
|
|
240
|
+
those commits carry `[skip ci]` and go straight to the default branch. An already-wired GitLab hub
|
|
241
|
+
picks the `resource_group` up on the next `yad update` / `yad check --fix`.
|
|
242
|
+
|
|
243
|
+
**`yad gate open` does not create or push the review branch.** It opens a PR/MR *against*
|
|
244
|
+
`review/<epic>/<artifact>`, so that branch must already be **on origin** — a local-only branch is no
|
|
245
|
+
use, because `gh pr create --head` does not push either. `yad open-pr`, run from the branch, pushes it
|
|
246
|
+
and then delegates here. Opening with the branch absent is refused up front, before any ledger write,
|
|
247
|
+
rather than failing inside `gh`/`glab`; an origin that cannot be reached at all only warns.
|
|
248
|
+
|
|
204
249
|
### Manual end-to-end verification (GitHub)
|
|
205
250
|
|
|
206
251
|
1. On a scratch hub: `yad setup` (platform github, roster with a second account) → `yad check --fix`
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
# RECONCILE (schedule, every 15 min): the safety net. The merge job's `closed` event fires once and
|
|
18
18
|
# never repeats, so a transient API/GraphQL/push failure (or a fail-closed degraded approval read)
|
|
19
19
|
# would otherwise strand a merged review until someone reran it by hand. This periodic job discovers
|
|
20
|
-
# recently-merged review PRs from the API and advances any not yet done
|
|
21
|
-
# `done` is
|
|
20
|
+
# recently-merged review PRs from the API and advances any not yet done. Idempotent: a step that is
|
|
21
|
+
# already `done` is never re-advanced (the chain is one-way), and re-syncing it writes nothing unless
|
|
22
|
+
# its approvals genuinely changed — so re-visiting a merged review for a week costs one no-op read per
|
|
23
|
+
# pass, not a commit. On GitHub a scheduled workflow runs automatically once committed (no setup).
|
|
22
24
|
#
|
|
23
25
|
# CI never approves and never merges — the merge click is the human approval act.
|
|
24
26
|
#
|
|
@@ -73,7 +75,8 @@ jobs:
|
|
|
73
75
|
|
|
74
76
|
reconcile:
|
|
75
77
|
# Safety net: recover any merged review PR whose merge-time run failed transiently (the `closed`
|
|
76
|
-
# event never re-fires). Idempotent — `yad gate ci`
|
|
78
|
+
# event never re-fires). Idempotent — `yad gate ci` never re-advances a step that is already `done`,
|
|
79
|
+
# and a re-sync of one writes nothing unless its approvals actually changed.
|
|
77
80
|
if: github.event_name == 'schedule'
|
|
78
81
|
runs-on: ubuntu-latest
|
|
79
82
|
concurrency:
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
# - MERGE push (default branch, commit names a review branch): resolve the merged MR's IID from its
|
|
20
20
|
# source branch, then advance + flip status on the default branch — near-immediate.
|
|
21
21
|
# - SCHEDULED sweep (catch-up): enumerate recently-merged review MRs via the API and advance any
|
|
22
|
-
# not yet advanced (idempotent —
|
|
22
|
+
# not yet advanced (idempotent — an already-`done` step is never re-advanced, and re-syncing it
|
|
23
|
+
# writes nothing unless its approvals changed). Covers bare approvals and
|
|
23
24
|
# squash merges whose commit message dropped the branch name. Create a pipeline schedule
|
|
24
25
|
# (one-time, cannot be committed as code): cron `*/15 * * * *` with variable SDLC_GATE_SYNC=true.
|
|
25
26
|
# UI: CI/CD > Schedules, or:
|
|
@@ -39,6 +40,15 @@ variables:
|
|
|
39
40
|
|
|
40
41
|
yad-gate-sync:
|
|
41
42
|
needs: []
|
|
43
|
+
# Serialize every advance: the merge-push pipeline and the scheduled sweep both commit the ledger
|
|
44
|
+
# and push the DEFAULT branch, and the sweep's 7-day window covers MRs the merge path is still
|
|
45
|
+
# processing — so without this they race on the same MR. Both produce the advance, one pushes, the
|
|
46
|
+
# loser rebases onto it and pushes the same commit message under a different SHA. That is where the
|
|
47
|
+
# duplicate `chore(gate): advance …` commits with shifting SHAs come from, and every one of them
|
|
48
|
+
# carries [skip ci] and lands directly on the default branch, so nothing validates the churn.
|
|
49
|
+
# `resource_group` is GitLab's equivalent of the `concurrency` group the GitHub workflow sets on both
|
|
50
|
+
# of its jobs: at most one instance runs at a time, the rest queue.
|
|
51
|
+
resource_group: yad-gate-mergesync
|
|
42
52
|
# Runner selection: set the YAD_RUNNER_TAGS CI/CD variable (e.g. `dind_runner`) to route this
|
|
43
53
|
# docker job on instances whose runners are tag-locked (run_untagged: false) — it lives in
|
|
44
54
|
# project Settings → CI/CD → Variables, so it survives every `yad` sync. When unset, current
|
|
@@ -69,7 +79,8 @@ yad-gate-sync:
|
|
|
69
79
|
rc=0
|
|
70
80
|
if [ "$CI_PIPELINE_SOURCE" = "schedule" ]; then
|
|
71
81
|
# SCHEDULED SWEEP — discover merged review MRs from the platform (Path B keeps no per-branch
|
|
72
|
-
# ledger), then advance each. `gate ci` is idempotent:
|
|
82
|
+
# ledger), then advance each. `gate ci` is idempotent: an already-`done` step is never
|
|
83
|
+
# re-advanced, and re-syncing it writes nothing unless its approvals changed.
|
|
73
84
|
# A stuck review MR (a squash merge whose commit dropped the branch name, or a failed merge
|
|
74
85
|
# push) is always RECENT, so sweep a generous recent window and PAGINATE it fully (--paginate)
|
|
75
86
|
# — this bounds cost without the old hard 50-row cap that could permanently strand older MRs.
|
|
@@ -131,8 +131,9 @@ via the local user's `gh`/`glab`. For each:
|
|
|
131
131
|
- an `APPROVED` review / MR approval → append an `approved` record to `approvals.json` tagged
|
|
132
132
|
`"source": "bridge"`; a `COMMENTED`/`CHANGES_REQUESTED`/note → write to
|
|
133
133
|
`reviews/<artifact-base>--<YYYY-MM-DD>--comments.md` + `comments.json` (never an approval).
|
|
134
|
-
**Idempotent:** upsert bridge approvals by `(step, approver, role, domain)`, supersede revoked ones
|
|
135
|
-
|
|
134
|
+
**Idempotent:** upsert bridge approvals by `(step, approver, role, domain)`, supersede revoked ones
|
|
135
|
+
**while the step is open** (a step already `done` keeps its approvals — they are the record of why it
|
|
136
|
+
passed), and key comments on the platform comment id (re-running `sync` does not duplicate). **Manual approvals (no
|
|
136
137
|
`source` tag) are never touched.** For the architecture+contract step, discard bridge approvals dated
|
|
137
138
|
before a new contract lock (re-lock invalidates platform approvals too). Then refresh the `approved.md`
|
|
138
139
|
roster, set `hub-prs.json` `lastSyncedAt`, and **re-evaluate Step 3**. Under the PR-driven CLI (`yad
|
|
@@ -170,7 +171,9 @@ If the predicate **passes**:
|
|
|
170
171
|
|
|
171
172
|
### PR-driven automation (the `yad gate` CLI)
|
|
172
173
|
When the hub has a platform, **CI is the sole writer of the ledger**. `yad gate open` opens the review
|
|
173
|
-
PR only
|
|
174
|
+
PR only — against the `review/<epic>/<artifact>` branch, which must already exist (create it and run
|
|
175
|
+
`yad open-pr` from it, which pushes it first). CI (`yad gate ci`) writes the `.sdlc/` + `reviews/`
|
|
176
|
+
records this skill describes. The skill's
|
|
174
177
|
job is the human half: presenting the artifact, helping the owner address comments, and narrating the
|
|
175
178
|
gate. Local `yad gate sync` is advisory in bridge mode (reads the platform, prints status, writes
|
|
176
179
|
nothing); a human must never commit gate-state files (the `ledger-guard` check rejects it).
|
|
@@ -179,6 +182,13 @@ Under that CLI the gate **advances on merge**: a review PR/MR whose reviewer rul
|
|
|
179
182
|
comment threads are **all resolved**, and which has been **merged** auto-marks the step `done` and
|
|
180
183
|
unblocks the next step. (Until those three hold, the step stays `in_review`.)
|
|
181
184
|
|
|
185
|
+
**Re-reviewing a step that already advanced.** A step is advanced **once** — the chain is never pulled
|
|
186
|
+
backward. But a step that is `done` is still *synced*: when the artifact is edited (for architecture, a
|
|
187
|
+
re-locked contract surface) its prior approvals go stale, and the approvals arriving on the new review
|
|
188
|
+
PR/MR are recorded and bound to the new content. `yad gate status` then shows the truth — how many
|
|
189
|
+
approvals are live against what is in the file today, and how many were revoked — instead of a `done`
|
|
190
|
+
step whose approvals all belong to the version before the edit.
|
|
191
|
+
|
|
182
192
|
The flow is **merge-driven** (wired by `yad-hub-bridge` `wire`): during review CI writes nothing — the
|
|
183
193
|
platform PR/MR is the source of truth (native approvals + threads), and CI never touches the review
|
|
184
194
|
branch (so an in-flight approval is never dismissed and required checks never strand). On the human
|
|
@@ -42,7 +42,9 @@ content. This prevents "approve, then quietly change it" (build plan §5 spirit)
|
|
|
42
42
|
For the architecture+contract review there is a second, content-based staleness check: recompute the
|
|
43
43
|
SHA-256 of the contract-surface block and compare it to `.sdlc/contract-lock.json`. A mismatch means
|
|
44
44
|
the locked surface changed even if the file's mtime looks fine — approvals are stale, re-lock and
|
|
45
|
-
re-approve. (Hash recipe: `yad-architecture/references/contract-format.md`.)
|
|
45
|
+
re-approve. (Hash recipe: `yad-architecture/references/contract-format.md`.) `yad` computes the
|
|
46
|
+
identical digest, so `yad doctor` performs this comparison for every epic and FAILs on a surface that
|
|
47
|
+
drifted from its lock — run it rather than recomputing by hand.
|
|
46
48
|
|
|
47
49
|
## Worked example — epic gate
|
|
48
50
|
|
|
@@ -91,8 +93,10 @@ approvals regardless of how they were recorded.
|
|
|
91
93
|
- On PR/MR open the assignee is the committer and reviewers are the scope's `reviewer` + `domain-owner`
|
|
92
94
|
members (minus the committer); the owner/author is recorded, not requested. See
|
|
93
95
|
`../yad-hub-bridge/references/login-roster.md`.
|
|
94
|
-
- `sync` is idempotent (upsert by `(step, approver, role, domain)`;
|
|
95
|
-
|
|
96
|
+
- `sync` is idempotent (upsert by `(step, approver, role, domain)`; key comments on comment id) and
|
|
97
|
+
never touches **manual** approvals. A revoked approval is superseded **while the step is open**; once
|
|
98
|
+
the step is `done` its approvals are kept as the record of why it passed, and a re-sync only re-binds
|
|
99
|
+
new ones (see `../yad-hub-bridge/references/bridge.md` → "Idempotent re-sync").
|
|
96
100
|
- The architecture+contract staleness rule applies to bridge approvals too: a re-lock discards bridge
|
|
97
101
|
approvals dated before the new lock.
|
|
98
102
|
- No platform / no CLI → the gate runs file-only with no error. Detail: `../yad-hub-bridge/references/bridge.md`.
|
|
@@ -68,7 +68,7 @@ story: EP-<slug>-S0N
|
|
|
68
68
|
epic: EP-<slug>
|
|
69
69
|
repo: <repo>
|
|
70
70
|
feature-id: EP-<slug>-S0N
|
|
71
|
-
product-repo: <absolute or relative
|
|
71
|
+
product-repo: <path to the product repo — absolute, or relative to THIS file's dir (specs/<story>/)>
|
|
72
72
|
contract-lock: sha256:<hex copied from epics/EP-<slug>/.sdlc/contract-lock.json>
|
|
73
73
|
speckit: installed | not-installed
|
|
74
74
|
generated: <YYYY-MM-DD>
|
|
@@ -86,6 +86,16 @@ The contract surface above is **referenced, not re-defined**. Any change to the
|
|
|
86
86
|
back to the architecture gate in the product repo — it is never widened from this code repo.
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
`product-repo` is the one field CI resolves on disk (contract-check, lineage-check, epic-open,
|
|
90
|
+
reconcile-debt all read it). Every gate resolves it the SAME way: an **absolute** path is used as-is; a
|
|
91
|
+
**relative** path is joined to this `link.md`'s own directory, `specs/<story>/` — `../../` climbs out
|
|
92
|
+
of the story dir and out of `specs/` to the code-repo root, so a hub checked out beside the code repo
|
|
93
|
+
is `../../../<hub-dir>`. A relative value that only resolves from the **repo root** still works too
|
|
94
|
+
(what contract-check historically did), so older `link.md` files keep gating. Write it as an
|
|
95
|
+
**unquoted scalar** — `"..."` or `'...'` is taken literally, and a leading `~`/`$VAR` is never
|
|
96
|
+
expanded. An unreachable path degrades the hub-reading gates to a PASS-with-note rather than failing
|
|
97
|
+
them, and each one now says so in its output.
|
|
98
|
+
|
|
89
99
|
## Do not re-invent the contract
|
|
90
100
|
|
|
91
101
|
The spec **quotes** the locked surface; it never extends it. To confirm the surface the spec relies on
|
|
@@ -93,7 +103,7 @@ matches the lock, run from the **product** repo:
|
|
|
93
103
|
|
|
94
104
|
```bash
|
|
95
105
|
awk '/CONTRACT-SURFACE:BEGIN/{f=1;next} /CONTRACT-SURFACE:END/{f=0} f' \
|
|
96
|
-
epics/EP-<slug>/contract.md | shasum -a 256
|
|
106
|
+
epics/EP-<slug>/contract.md | tr -d '\r' | shasum -a 256
|
|
97
107
|
# compare against epics/EP-<slug>/.sdlc/contract-lock.json
|
|
98
108
|
```
|
|
99
109
|
|