yadflow 3.15.0 → 3.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +1 -1
- package/skills/yad-analysis/SKILL.md +4 -0
- package/skills/yad-backfill/SKILL.md +3 -2
- package/skills/yad-backfill/templates/checks/backfill-check.sh +26 -2
- package/skills/yad-change/SKILL.md +6 -0
- package/skills/yad-checks/SKILL.md +10 -1
- package/skills/yad-checks/references/check-gates.md +43 -2
- package/skills/yad-checks/templates/checks/commit-message.sh +24 -1
- package/skills/yad-checks/templates/checks/contract-check.sh +70 -11
- package/skills/yad-checks/templates/checks/epic-open.sh +24 -1
- package/skills/yad-checks/templates/checks/ledger-guard.sh +102 -4
- package/skills/yad-checks/templates/checks/lineage-check.sh +24 -1
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +24 -1
- package/skills/yad-checks/templates/checks/spec-link.sh +24 -1
- package/skills/yad-checks/templates/checks/verified-commits.sh +24 -1
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +3 -1
- package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +3 -1
- package/skills/yad-discovery/SKILL.md +4 -0
- package/skills/yad-docs/templates/app/package-lock.json +3 -3
- package/skills/yad-epic/SKILL.md +4 -0
- package/skills/yad-epic/references/state-schema.md +10 -0
- package/skills/yad-hub-bridge/SKILL.md +9 -3
- package/skills/yad-hub-bridge/references/bridge.md +18 -3
- package/skills/yad-review-gate/SKILL.md +3 -1
- package/skills/yad-stub/SKILL.md +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## [3.15.2](https://github.com/abdelrahmannasr/yadflow/compare/v3.15.1...v3.15.2) (2026-08-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **checks:** exempt a new epic's ledger seed from ledger-guard ([ba923c2](https://github.com/abdelrahmannasr/yadflow/commit/ba923c2a3823e8bf17b2fc59b41f0160a3a11a19)), closes [#162](https://github.com/abdelrahmannasr/yadflow/issues/162)
|
|
7
|
+
|
|
8
|
+
## [3.15.1](https://github.com/abdelrahmannasr/yadflow/compare/v3.15.0...v3.15.1) (2026-08-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **checks:** close the silent-PASS holes the [#161](https://github.com/abdelrahmannasr/yadflow/issues/161) fix left open ([0f180ab](https://github.com/abdelrahmannasr/yadflow/commit/0f180abc72618fcd793ef6717fe1e128aa5ccfa5))
|
|
14
|
+
* **checks:** pin every changed contract slice, not just the first ([a79a946](https://github.com/abdelrahmannasr/yadflow/commit/a79a946aa37c08615741082bc638d006f7e80b76)), closes [#161](https://github.com/abdelrahmannasr/yadflow/issues/161)
|
|
15
|
+
* **checks:** read hub.json and the contract lock across line breaks ([43a618d](https://github.com/abdelrahmannasr/yadflow/commit/43a618d0822ed8576b9a164889bc53a52bbdf713)), closes [#161](https://github.com/abdelrahmannasr/yadflow/issues/161)
|
|
16
|
+
|
|
1
17
|
# [3.15.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.14.0...v3.15.0) (2026-08-10)
|
|
2
18
|
|
|
3
19
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "3.15.
|
|
3
|
+
"version": "3.15.2",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo, thread, reconcile). A BMAD module + 38 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
|
@@ -138,6 +138,10 @@ Notes:
|
|
|
138
138
|
- Also create an empty approvals ledger `{project-root}/epics/EP-<slug>/.sdlc/approvals.json`
|
|
139
139
|
and an empty comments ledger `{project-root}/epics/EP-<slug>/.sdlc/comments.json`, each containing
|
|
140
140
|
`[]`, and the `reviews/` directory.
|
|
141
|
+
- Commit the seed on the `analysis/EP-<slug>` branch, and cut `review/EP-<slug>/analysis` from it so the
|
|
142
|
+
epic's **first** review PR/MR carries the ledger to the default branch. In bridge mode `ledger-guard`
|
|
143
|
+
exempts a new epic's ledger (creation, not mutation, #162); every later change to it is CI's. See
|
|
144
|
+
`../yad-epic/references/state-schema.md`, "Authoring branches".
|
|
141
145
|
|
|
142
146
|
### Step 7 — Stop at the gate (do NOT advance)
|
|
143
147
|
Report: epic ID, the path to `analysis.md`, and that the next action is **review** via
|
|
@@ -72,8 +72,9 @@ the approver(s) + date. Only a `verified: true` backfill spec counts as real.
|
|
|
72
72
|
|
|
73
73
|
### Step 5 — `gate` (block changes per touched feature)
|
|
74
74
|
`bash checks/backfill-check.sh <base>` blocks a change that touches a feature being backfilled until
|
|
75
|
-
that feature's spec is `verified: true`.
|
|
76
|
-
|
|
75
|
+
that feature's spec is `verified: true`. `<base>` is optional and resolves like every other gate's —
|
|
76
|
+
see "Resolving `<base>`" in `../yad-checks/references/check-gates.md`. It is **per touched feature** —
|
|
77
|
+
a change touching feature A is not blocked by an unverified feature B. Forward-spec'd features (those with their own `specs/<story>/`)
|
|
77
78
|
are not this gate's concern.
|
|
78
79
|
|
|
79
80
|
### Step 6 — `promote` (flip a stub epic → real, once its spec is approved)
|
|
@@ -5,13 +5,37 @@
|
|
|
5
5
|
# forward-spec'd (their own specs/<story>/) or not yet being backfilled are not this gate's concern.
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
9
|
+
# --- is duplicated, not sourced) ---
|
|
10
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
11
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
12
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
13
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
14
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
15
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
16
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
17
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
18
|
+
# command substitution in an assignment aborts the script.
|
|
19
|
+
resolve_base() {
|
|
20
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
21
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
22
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
23
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
24
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
25
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
26
|
+
done
|
|
27
|
+
printf '%s' origin/main
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
31
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [backfill]: no base given — diffing against '${BASE}'."
|
|
9
32
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
10
33
|
echo "FAIL [backfill]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
11
34
|
exit 1
|
|
12
35
|
fi
|
|
13
36
|
|
|
14
|
-
|
|
37
|
+
# quotePath off so a non-ASCII feature directory still matches src/<feature>/ below.
|
|
38
|
+
changed="$(git -c core.quotePath=false diff --name-only "${BASE}..HEAD")"
|
|
15
39
|
# Feature = a directory under src/ (src/<feature>/...). Top-level src/*.js files are deliberately NOT
|
|
16
40
|
# gated here (they belong to no single feature); only src/<feature>/ changes are checked.
|
|
17
41
|
feats="$(printf '%s\n' "$changed" | sed -nE 's#^src/([^/]+)/.*#\1#p' | sort -u)"
|
|
@@ -134,6 +134,12 @@ Seed `.sdlc/approvals.json` with one **provenance** record per inherited gate (N
|
|
|
134
134
|
`{ "artifact": "<art>", "step": "<…-review>", "status": "inherited", "from": "<epic>", "boundHash": "<hash>", "date": "<today>" }`.
|
|
135
135
|
Seed `.sdlc/comments.json` = `[]` and create `reviews/`.
|
|
136
136
|
|
|
137
|
+
Commit the seed on the `change/EP-<slug>` branch. It reaches the hub's default branch through this
|
|
138
|
+
change-epic's **first** review PR/MR — cut the `review/EP-<slug>/<artifact>` branch from `change/…` so
|
|
139
|
+
it carries the seed. In bridge mode `ledger-guard` exempts a new epic's ledger (creation, not mutation,
|
|
140
|
+
#162), so no direct push to a protected default branch is needed; every later change to that ledger is
|
|
141
|
+
CI's. See `../yad-epic/references/state-schema.md`, "Authoring branches".
|
|
142
|
+
|
|
137
143
|
When `architecture` is **inherited**, materialize the **pointer-lock** `.sdlc/contract-lock.json`:
|
|
138
144
|
`{ "artifact": "contract.md", "hash": "<parent surface hash, verbatim>", "lockedAt": "<today>", "inheritedFrom": "<epic>", "ref": "../../<epic>/.sdlc/contract-lock.json" }`.
|
|
139
145
|
There is no `contract.md` in the change-epic, so the surface cannot drift, and `contract-check` passes
|
|
@@ -15,6 +15,7 @@ in CI on every PR/MR and must pass before merge (build plan §C). Each is a smal
|
|
|
15
15
|
`specs/<story>/contracts/`) without a `Contract-Change: yes` trailer **and** an updated, re-locked
|
|
16
16
|
contract upstream, it **FAILS and routes back to the architecture gate**. The shared surface is
|
|
17
17
|
never widened from inside a code repo (Phase 2 contract representation: delimited block + SHA-256 lock).
|
|
18
|
+
Every story whose slice the diff touches is checked, not just the first — see `references/check-gates.md`.
|
|
18
19
|
3. **build/test/lint** — standard quality stage; tests must actually exercise new behavior, not just pass.
|
|
19
20
|
The CI job sets `YAD_TEST_MAX_WORKERS` (default `2`); the gate caps jest/vitest test concurrency at
|
|
20
21
|
that and is a no-op for other runners (see `references/check-gates.md`).
|
|
@@ -54,7 +55,11 @@ and GitLab CI. This step is **by hand** in Phase 3 — run the gates with the sk
|
|
|
54
55
|
when humans legitimately own the ledger). On review PRs it FAILs any commit that touches the
|
|
55
56
|
CI-owned gate ledger (`.sdlc/{state,approvals,comments,hub-prs}.json`, `reviews/*.md`) unless it
|
|
56
57
|
is a **verified gate-bot commit** — bot-authored AND platform-Verified, since author text alone is
|
|
57
|
-
spoofable. `.sdlc/contract-lock.json` is artifact-side and exempt.
|
|
58
|
+
spoofable. `.sdlc/contract-lock.json` is artifact-side and exempt. So is a **new epic's seed**:
|
|
59
|
+
no CI path can create a ledger (`gate ci` only *advances* an existing chain, at merge, on the
|
|
60
|
+
default branch), so an epic whose `.sdlc/state.json` is absent from the base ref may be created by
|
|
61
|
+
a human on its first review PR/MR — **creation, not mutation** (#162). Once the ledger is on the
|
|
62
|
+
default branch the guard is absolute again. Runs in `yad-hub-checks`
|
|
58
63
|
alongside `verified-commits` (which waives the allowlist for the bot but still requires its
|
|
59
64
|
signature). See `yad-hub-bridge`.
|
|
60
65
|
- `templates/github/yad-verified-commits.yml` + `templates/gitlab/yad-verified-commits.gitlab-ci.yml`
|
|
@@ -141,6 +146,10 @@ bash checks/spec-link.sh "<base>"
|
|
|
141
146
|
bash checks/contract-check.sh "<base>"
|
|
142
147
|
bash checks/build-test-lint.sh
|
|
143
148
|
```
|
|
149
|
+
`<base>` is optional for the gates that take one (`build-test-lint` takes none) — omitted, the gate
|
|
150
|
+
resolves the trunk (configured `default_branch`, else `origin/HEAD`, else `origin/main`) and prints
|
|
151
|
+
the base it chose; pass it (or `SDLC_BASE`) when the PR/MR targets another branch.
|
|
152
|
+
|
|
144
153
|
A non-zero exit is a FAIL. Summarize which gates passed and, for any failure, the exact remediation
|
|
145
154
|
(spec-link: add the `Task:` trailer / spec; contract-check: route back to the architecture gate and
|
|
146
155
|
re-lock the contract; build/test/lint: fix the failing lint/test).
|
|
@@ -19,6 +19,27 @@ repo uses. Each reads conventions established by earlier steps — it invents no
|
|
|
19
19
|
| pr-title | the PR/MR title (from the CI event payload) | `yad-pr-template` (`config.yaml build.pr_title_style`) |
|
|
20
20
|
| pr-template | the PR/MR body (from the CI event payload) | `yad-pr-template` (the committed PR/MR template) |
|
|
21
21
|
|
|
22
|
+
## Resolving `<base>` (every gate that takes one)
|
|
23
|
+
|
|
24
|
+
The `<base>` argument is **optional**. The order is: the **argument**, else `SDLC_BASE`, else the
|
|
25
|
+
**configured** `default_branch` (`.sdlc/hub.json`, or `SDLC_HUB_CONFIG`), else the remote's
|
|
26
|
+
**published default branch** (`git symbolic-ref refs/remotes/origin/HEAD`), else `origin/main` —
|
|
27
|
+
the same order the CLI resolves (`cli/hubcommit.mjs`, `cli/repo.mjs`), so a gate never diffs a
|
|
28
|
+
different range than the `yad` commands run beside it. Each candidate must actually **resolve**
|
|
29
|
+
before it is used, so a *dangling* `origin/HEAD` (trunk renamed, the old remote-tracking ref pruned)
|
|
30
|
+
falls through instead of failing the gate on a fully-fetched repo. CI always passes the base
|
|
31
|
+
explicitly (`origin/<PR base>` / `origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`), so this governs
|
|
32
|
+
local runs.
|
|
33
|
+
|
|
34
|
+
Hardcoding `origin/main` diffed the wrong range on a repo whose trunk is `develop`/`master` — either
|
|
35
|
+
failing closed for the wrong reason, or, where a stale `main` still existed, silently diffing a range
|
|
36
|
+
that both mis-reports the surface and drags unrelated stories into contract-check's per-story fidelity
|
|
37
|
+
pass (issue #161). An auto-resolved base is **printed as a note**, so the range a local run gated is
|
|
38
|
+
never implicit. Like the `product-repo` block below, this one is duplicated verbatim across the
|
|
39
|
+
scripts (they are standalone by design) and pinned byte-identical by a test — which covers every
|
|
40
|
+
base-taking gate, including `yad-backfill`'s `backfill-check.sh` and the installed copies this repo's
|
|
41
|
+
own CI runs, plus an assertion that each one actually *assigns* `BASE` from it.
|
|
42
|
+
|
|
22
43
|
## 1. spec-link (`templates/checks/spec-link.sh`)
|
|
23
44
|
|
|
24
45
|
- Checks every non-merge commit in `<base>..HEAD` **per commit** (not aggregated across the range),
|
|
@@ -38,11 +59,13 @@ repo uses. Each reads conventions established by earlier steps — it invents no
|
|
|
38
59
|
- An empty range (no non-merge commits) **PASSes**.
|
|
39
60
|
- Portable across bash 3.2 (macOS) and 4+ (no `mapfile`).
|
|
40
61
|
- **Fails closed** when `<base>` can't be resolved (so a shallow clone / wrong base never PASSes blind).
|
|
62
|
+
`<base>` is optional — see [Resolving `<base>`](#resolving-base-every-gate-that-takes-one).
|
|
41
63
|
|
|
42
64
|
## 2. contract-check (`templates/checks/contract-check.sh`)
|
|
43
65
|
|
|
44
66
|
- **Fails closed** if `<base>` can't be resolved — an undiffable range must never report "no surface
|
|
45
|
-
change" and silently green-light a bypass.
|
|
67
|
+
change" and silently green-light a bypass. `<base>` is optional — see
|
|
68
|
+
[Resolving `<base>`](#resolving-base-every-gate-that-takes-one).
|
|
46
69
|
- Computes the changed files in `<base>..HEAD`.
|
|
47
70
|
- If **nothing** under `specs/*/contracts/**` changed → **PASS** (normal implementation only *consumes*
|
|
48
71
|
the contract).
|
|
@@ -52,6 +75,19 @@ repo uses. Each reads conventions established by earlier steps — it invents no
|
|
|
52
75
|
require `link.md`'s pinned `contract-lock` hash to match the product repo's current
|
|
53
76
|
`contract-lock.json`. A claimed change that still pins the **old** lock **FAILS** — re-run
|
|
54
77
|
`yad-spec` so the slice matches the re-locked contract.
|
|
78
|
+
- The fidelity check runs for **every story whose slice the diff touches**, and **aggregates**: each
|
|
79
|
+
story reports (matched / stale / deferred), and any stale pin fails the gate. `git diff
|
|
80
|
+
--name-only` is path-sorted, so reading one story off the first changed path validated whichever
|
|
81
|
+
story sorted first and left the rest unpinned — a later story pinning a stale hash passed, and a
|
|
82
|
+
first story with no `link.md` deferred the whole check before the stale one was ever read
|
|
83
|
+
(issue #161). One clean-or-deferred story never masks another's stale pin, the same rule spec-link
|
|
84
|
+
applies per commit.
|
|
85
|
+
- A lock the gate can **read but not parse** (truncated, half-written, or a changed schema — no
|
|
86
|
+
`"hash": "sha256:…"`) **FAILS**. It used to short-circuit the comparison into the "hash matches"
|
|
87
|
+
note, i.e. the gate affirmatively reported a match it never made.
|
|
88
|
+
- The changed-file list is read with `core.quotePath=false`. With git's default, a path holding a
|
|
89
|
+
non-ASCII byte comes back quoted and octal-escaped, so a slice like `specs/EP-démo-S01/contracts/…`
|
|
90
|
+
never matched the surface pattern and an undeclared widening passed untouched.
|
|
55
91
|
- This enforces the Phase 2 rule: the shared surface is owned upstream and is never widened from inside
|
|
56
92
|
a code repo. The hash recipe is in `../yad-architecture/references/contract-format.md`.
|
|
57
93
|
|
|
@@ -129,6 +165,7 @@ non-merge commit in `<base>..HEAD`:
|
|
|
129
165
|
- **Profiles** (`--profile code|hub`): the subject rule is identical on both; the gate never requires
|
|
130
166
|
the `Task:` trailer (spec-link owns that on code repos; hub commits are not task-scoped).
|
|
131
167
|
- **Fails closed** when `<base>` can't be resolved.
|
|
168
|
+
`<base>` is optional — see [Resolving `<base>`](#resolving-base-every-gate-that-takes-one).
|
|
132
169
|
|
|
133
170
|
## 6. pr-title (`templates/checks/pr-title.sh`)
|
|
134
171
|
|
|
@@ -319,9 +356,13 @@ line). Code repos run the same three with `--profile code` inside the main `yad-
|
|
|
319
356
|
|
|
320
357
|
## Running by hand (Phase 3 is manual)
|
|
321
358
|
|
|
322
|
-
From inside the code repo, against the PR/MR base (e.g. `master`)
|
|
359
|
+
From inside the code repo, against the PR/MR base (e.g. `master`). For the gates that take one, the
|
|
360
|
+
base argument is optional — omit it and the gate resolves the trunk in the order above (configured
|
|
361
|
+
`default_branch`, else `origin/HEAD`, else `origin/main`), printing the base it chose; pass it (or
|
|
362
|
+
`export SDLC_BASE=…`) whenever the PR targets something else. `build-test-lint` takes no base at all.
|
|
323
363
|
|
|
324
364
|
```bash
|
|
365
|
+
bash checks/spec-link.sh # -> diffs the resolved trunk, and says which one
|
|
325
366
|
bash checks/spec-link.sh master
|
|
326
367
|
bash checks/contract-check.sh master
|
|
327
368
|
bash checks/build-test-lint.sh
|
|
@@ -25,7 +25,30 @@ while [ $# -gt 0 ]; do
|
|
|
25
25
|
done
|
|
26
26
|
case "$PROFILE" in code|hub) ;; *) echo "FAIL [commit-message]: unknown --profile '$PROFILE' (code|hub)."; exit 1 ;; esac
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
29
|
+
# --- is duplicated, not sourced) ---
|
|
30
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
31
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
32
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
33
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
34
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
35
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
36
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
37
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
38
|
+
# command substitution in an assignment aborts the script.
|
|
39
|
+
resolve_base() {
|
|
40
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
41
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
42
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
43
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
44
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
45
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
46
|
+
done
|
|
47
|
+
printf '%s' origin/main
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
BASE="${ARGS[0]:-${SDLC_BASE:-$(resolve_base)}}"
|
|
51
|
+
[ -n "${ARGS[0]:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [commit-message]: no base given — diffing against '${BASE}'."
|
|
29
52
|
|
|
30
53
|
# Fail closed if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
|
|
31
54
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
@@ -8,7 +8,30 @@
|
|
|
8
8
|
# architecture gate. Normal implementation that only CONSUMES the contract passes untouched.
|
|
9
9
|
set -euo pipefail
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
12
|
+
# --- is duplicated, not sourced) ---
|
|
13
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
14
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
15
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
16
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
17
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
18
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
19
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
20
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
21
|
+
# command substitution in an assignment aborts the script.
|
|
22
|
+
resolve_base() {
|
|
23
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
24
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
25
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
26
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
27
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
28
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
29
|
+
done
|
|
30
|
+
printf '%s' origin/main
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
34
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [contract-check]: no base given — diffing against '${BASE}'."
|
|
12
35
|
|
|
13
36
|
# Fail CLOSED if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
|
|
14
37
|
# Never let an undiffable range silently report "no surface change" — that would green-light a bypass.
|
|
@@ -18,7 +41,10 @@ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
|
18
41
|
fi
|
|
19
42
|
RANGE="${BASE}..HEAD"
|
|
20
43
|
|
|
21
|
-
|
|
44
|
+
# core.quotePath=false: with the default ON, git wraps any path holding a non-ASCII byte in quotes
|
|
45
|
+
# and octal-escapes it, so a slice like specs/EP-démo-S01/contracts/api.md never matches the pattern
|
|
46
|
+
# below — the surface change would be invisible to the gate it exists to stop.
|
|
47
|
+
changed="$(git -c core.quotePath=false diff --name-only "$RANGE")"
|
|
22
48
|
surface="$(printf '%s\n' "$changed" | grep -E '^specs/[^/]+/contracts/' || true)"
|
|
23
49
|
|
|
24
50
|
if [ -z "$surface" ]; then
|
|
@@ -72,9 +98,22 @@ resolve_product() {
|
|
|
72
98
|
|
|
73
99
|
# Fidelity check (best-effort): when the product repo is reachable, the story's link.md must pin the
|
|
74
100
|
# CURRENT product lock — proof the contract was actually updated/re-locked upstream, not just flagged.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
101
|
+
#
|
|
102
|
+
# Checked for EVERY story whose slice the diff touches, not just the first one. `git diff --name-only`
|
|
103
|
+
# is path-sorted, so reading a single story off `head -1` validated whichever story sorted first and
|
|
104
|
+
# left the rest unpinned: a second story pinning a STALE hash passed, and a first story with no
|
|
105
|
+
# link.md deferred the whole check before the stale one was ever read (issue #161). Failures are
|
|
106
|
+
# AGGREGATED — every story reports, so one clean-or-deferred story never masks another's stale pin
|
|
107
|
+
# (the same rule spec-link applies per commit).
|
|
108
|
+
stories="$(printf '%s\n' "$surface" | sed -E 's#^specs/([^/]+)/contracts/.*#\1#' | sort -u)"
|
|
109
|
+
rc=0
|
|
110
|
+
while IFS= read -r story; do
|
|
111
|
+
[ -z "$story" ] && continue
|
|
112
|
+
link="specs/${story}/link.md"
|
|
113
|
+
if [ ! -f "$link" ]; then
|
|
114
|
+
echo "note [contract-check]: no ${link} — fidelity check deferred (spec-link gates the link itself)."
|
|
115
|
+
continue
|
|
116
|
+
fi
|
|
78
117
|
product_rel="$(link_val product-repo "$link")"
|
|
79
118
|
pinned="$(printf '%s' "$(link_val contract-lock "$link")" | sed -E 's/^sha256:([0-9a-f]+).*$/\1/')"
|
|
80
119
|
epic="$(printf '%s' "$story" | sed -E 's/-S[0-9]+$//')" # story EP-<slug>-S0N -> epic EP-<slug>
|
|
@@ -85,20 +124,40 @@ if [ -f "$link" ]; then
|
|
|
85
124
|
lock=""
|
|
86
125
|
[ -n "$prod" ] && lock="${prod}/epics/${epic}/.sdlc/contract-lock.json"
|
|
87
126
|
if [ -n "$product_rel" ] && [ -f "$lock" ]; then
|
|
88
|
-
|
|
89
|
-
|
|
127
|
+
# Newline-tolerant, first-match: `"hash":` and its value may legally sit on separate lines, and an
|
|
128
|
+
# unparseable lock is a FAIL below — so a formatting choice must not become a gate failure.
|
|
129
|
+
# `|| current=""` is load-bearing: under `pipefail` a no-match grep fails the whole pipeline, which
|
|
130
|
+
# under `set -e` would abort the gate instead of reaching the unparseable-lock FAIL below.
|
|
131
|
+
current="$(tr '\n' ' ' < "$lock" | grep -oE '"hash"[[:space:]]*:[[:space:]]*"sha256:[0-9a-f]+"' | head -1 | sed -E 's/.*sha256:([0-9a-f]+)"$/\1/')" || current=""
|
|
132
|
+
# A lock we can READ but cannot PARSE proves nothing, and an empty `current` used to short-circuit
|
|
133
|
+
# the comparison below straight into the "hash matches" note — the gate affirmatively reporting a
|
|
134
|
+
# match it never made. Fail closed instead: a truncated, half-written or schema-changed lock is a
|
|
135
|
+
# broken lock, and a Contract-Change is being claimed against it.
|
|
136
|
+
if [ -z "$current" ]; then
|
|
137
|
+
echo "FAIL [contract-check]: ${lock} has no readable \"hash\": \"sha256:…\" value —"
|
|
138
|
+
echo " the lock cannot prove ${link}'s pin. Re-lock the contract upstream (yad-architecture Step 5)."
|
|
139
|
+
rc=1
|
|
140
|
+
continue
|
|
141
|
+
fi
|
|
142
|
+
if [ "$current" != "$pinned" ]; then
|
|
90
143
|
echo "FAIL [contract-check]: Contract-Change claimed, but ${link} still pins ${pinned:0:12}…"
|
|
91
144
|
echo " while the product lock is ${current:0:12}… — re-run yad-spec so the slice matches the re-locked contract."
|
|
92
|
-
|
|
145
|
+
rc=1
|
|
146
|
+
continue
|
|
93
147
|
fi
|
|
94
|
-
echo "note [contract-check]: link
|
|
148
|
+
echo "note [contract-check]: ${link} hash matches the product lock (${current:0:12}…)."
|
|
95
149
|
else
|
|
96
150
|
# Say so. A skipped fidelity check used to be indistinguishable from a passed one, which is how a
|
|
97
151
|
# mis-resolved product-repo could turn a stale-pin FAIL into a silent PASS (issue #149).
|
|
98
152
|
echo "note [contract-check]: product lock not reachable at ${lock:-<no product-repo in link.md>} — fidelity check deferred."
|
|
99
153
|
fi
|
|
100
|
-
|
|
101
|
-
|
|
154
|
+
done <<EOF
|
|
155
|
+
$stories
|
|
156
|
+
EOF
|
|
157
|
+
|
|
158
|
+
if [ "$rc" != 0 ]; then
|
|
159
|
+
echo "FAIL [contract-check]: a changed slice pins a stale contract lock (see above) — the surface was not re-locked upstream for every story in this diff."
|
|
160
|
+
exit 1
|
|
102
161
|
fi
|
|
103
162
|
|
|
104
163
|
echo "PASS [contract-check]: surface change accompanied by Contract-Change: yes (and an updated contract)."
|
|
@@ -11,7 +11,30 @@
|
|
|
11
11
|
# Fails CLOSED on an unresolvable base.
|
|
12
12
|
set -euo pipefail
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
15
|
+
# --- is duplicated, not sourced) ---
|
|
16
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
17
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
18
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
19
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
20
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
21
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
22
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
23
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
24
|
+
# command substitution in an assignment aborts the script.
|
|
25
|
+
resolve_base() {
|
|
26
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
27
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
28
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
29
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
30
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
31
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
32
|
+
done
|
|
33
|
+
printf '%s' origin/main
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
37
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [epic-open]: no base given — diffing against '${BASE}'."
|
|
15
38
|
|
|
16
39
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
17
40
|
echo "FAIL [epic-open]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
# NOT protected:
|
|
12
12
|
# epics/*/.sdlc/contract-lock.json — artifact-side: the architect locks the contract surface in
|
|
13
13
|
# `gate open`, so a human legitimately commits it alongside the architecture artifact.
|
|
14
|
+
# A brand-new epic's ledger — CREATION is not mutation (#162). No CI path can seed one, so the
|
|
15
|
+
# seed rides the first review PR/MR; see the carve-out below. Mutation stays bot-only.
|
|
14
16
|
#
|
|
15
17
|
# A "bot commit" must be BOTH authored by the gate bot (name/email contains yad-gate-sync) AND
|
|
16
18
|
# platform-VERIFIED — author/committer text alone is user-controlled and spoofable, so the platform
|
|
@@ -32,7 +34,30 @@ if [ ! -f "$HUB" ] || ! grep -Eq '"(bridge_enabled|bridge)"[[:space:]]*:[[:space
|
|
|
32
34
|
exit 0
|
|
33
35
|
fi
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
38
|
+
# --- is duplicated, not sourced) ---
|
|
39
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
40
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
41
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
42
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
43
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
44
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
45
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
46
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
47
|
+
# command substitution in an assignment aborts the script.
|
|
48
|
+
resolve_base() {
|
|
49
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
50
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
51
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
52
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
53
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
54
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
55
|
+
done
|
|
56
|
+
printf '%s' origin/main
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
60
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [ledger-guard]: no base given — diffing against '${BASE}'."
|
|
36
61
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
37
62
|
echo "FAIL [ledger-guard]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
38
63
|
exit 1
|
|
@@ -91,19 +116,81 @@ trusted_bot() {
|
|
|
91
116
|
signature_verified "$1"
|
|
92
117
|
}
|
|
93
118
|
|
|
119
|
+
# ---- seeding carve-out: creation is not mutation (#162) ---------------------------------------
|
|
120
|
+
# A brand-new epic's ledger has no CI author. `gate ci` only ADVANCES an existing chain — it bails on
|
|
121
|
+
# a missing state.json ("the review branch is cut from the default branch, so it should carry it") and
|
|
122
|
+
# writes only at merge, on the default branch — and the engine itself reads a missing state.json as
|
|
123
|
+
# "not seeded yet". `gate open` writes nothing in bridge mode, and `checkpoint` stages back-half
|
|
124
|
+
# ledgers only. So the seed the authoring skills write (yad-epic / yad-change / yad-analysis /
|
|
125
|
+
# yad-discovery / yad-stub) can reach the trunk ONLY through the first review PR/MR — the one place
|
|
126
|
+
# this gate runs. Guarding it there makes the documented flow unshippable on a protected trunk, so an
|
|
127
|
+
# epic whose ledger is absent from the BASE ref is exempt: that ledger is human-authored by
|
|
128
|
+
# construction and the reviewer sees the whole of it in the diff. The moment its state.json is on the
|
|
129
|
+
# base ref the guard is absolute again.
|
|
130
|
+
#
|
|
131
|
+
# Anchored on state.json — the ledger root, the same "is this epic seeded?" question the engine asks —
|
|
132
|
+
# NOT on the individual file: adding hub-prs.json to an epic that IS on the base ref still FAILs.
|
|
133
|
+
# Anchored on the BASE ref, not the parent commit, so delete-then-re-add cannot reset the exemption
|
|
134
|
+
# (the deletion is itself a guarded change).
|
|
135
|
+
#
|
|
136
|
+
# The slug match is CASE-FOLDED on purpose. Git paths are byte-exact, but macOS/Windows checkouts are
|
|
137
|
+
# not: seeding `epics/ep-x/.sdlc/state.json` beside an on-base `epics/EP-X/` would probe as a brand-new
|
|
138
|
+
# epic, pass, and then land ON TOP of the real ledger in every case-insensitive clone — a mutation
|
|
139
|
+
# laundered as a creation. So the base's seeded slugs are read once (one ls-tree, not one probe per
|
|
140
|
+
# path) and compared folded.
|
|
141
|
+
#
|
|
142
|
+
# Slugs are held in ARRAYS read from NUL-delimited git output, never in a space- or newline-delimited
|
|
143
|
+
# string: git permits a newline inside a path, and a `EP-<newline>x` slug split across two records
|
|
144
|
+
# would drop the real epic out of the on-base list and let a mutation through as a "creation". Both
|
|
145
|
+
# arrays carry one empty sentinel element so `"${a[@]}"` is safe under `set -u` on bash 3.2 (macOS),
|
|
146
|
+
# which has no associative arrays; a slug is never empty, so the sentinel can never match.
|
|
147
|
+
base_slugs=("") # every epic with a ledger on BASE, lowercased
|
|
148
|
+
base_slugs_loaded=0
|
|
149
|
+
noted_slugs=("") # slugs already announced, so the note prints once each
|
|
150
|
+
fold() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
|
151
|
+
in_list() { # $1 = needle, $2… = haystack
|
|
152
|
+
_needle="$1"; shift
|
|
153
|
+
for _item in "$@"; do [ "$_item" = "$_needle" ] && return 0; done
|
|
154
|
+
return 1
|
|
155
|
+
}
|
|
156
|
+
is_seeding() { # $1 = epic slug; 0 when that epic has no ledger on BASE
|
|
157
|
+
if [ "$base_slugs_loaded" = 0 ]; then
|
|
158
|
+
while IFS= read -r -d '' _p; do
|
|
159
|
+
case "$_p" in
|
|
160
|
+
epics/*/.sdlc/state.json)
|
|
161
|
+
_s="${_p#epics/}"; _s="${_s%%/*}"
|
|
162
|
+
base_slugs[${#base_slugs[@]}]="$(fold "$_s")"
|
|
163
|
+
;;
|
|
164
|
+
esac
|
|
165
|
+
done < <(git -c core.quotePath=false ls-tree -r --name-only -z "${BASE}" -- epics 2>/dev/null || true)
|
|
166
|
+
base_slugs_loaded=1
|
|
167
|
+
fi
|
|
168
|
+
_f="$(fold "$1")"
|
|
169
|
+
in_list "$_f" "${base_slugs[@]}" && return 1
|
|
170
|
+
in_list "$_f" "${noted_slugs[@]}" && return 0
|
|
171
|
+
noted_slugs[${#noted_slugs[@]}]="$_f"
|
|
172
|
+
echo "note [ledger-guard]: epics/$1 has no ledger on ${BASE} — new epic, its seed is exempt (creation, not mutation)."
|
|
173
|
+
return 0
|
|
174
|
+
}
|
|
175
|
+
|
|
94
176
|
violations=0
|
|
95
177
|
for sha in $commits; do
|
|
96
178
|
touches_ledger=0
|
|
97
|
-
|
|
179
|
+
# quotePath=false + -z on both git reads: a path git chose to escape ("epics/EP-caf\303\251/…") and a
|
|
180
|
+
# path holding a newline both match no arm below, so the gate would fail OPEN on exactly the paths it
|
|
181
|
+
# is meant to guard. NUL is the only byte a git path cannot contain.
|
|
182
|
+
while IFS= read -r -d '' f; do
|
|
98
183
|
[ -n "$f" ] || continue
|
|
99
184
|
case "$f" in
|
|
100
185
|
epics/*/.sdlc/contract-lock.json) ;; # artifact-side — allowed
|
|
101
186
|
epics/*/.sdlc/state.json|epics/*/.sdlc/approvals.json|epics/*/.sdlc/comments.json|epics/*/.sdlc/hub-prs.json|epics/*/reviews/*.md)
|
|
187
|
+
_slug="${f#epics/}"; _slug="${_slug%%/*}"
|
|
188
|
+
is_seeding "$_slug" && continue # a new epic's seed — not a mutation of a CI-owned ledger
|
|
102
189
|
touches_ledger=1
|
|
103
190
|
echo " ${sha} (author $(git show -s --format='%an' "$sha")) → $f"
|
|
104
191
|
;;
|
|
105
192
|
esac
|
|
106
|
-
done < <(git diff-tree --no-commit-id --name-only -r "$sha")
|
|
193
|
+
done < <(git -c core.quotePath=false diff-tree --no-commit-id --name-only -r -z "$sha")
|
|
107
194
|
if [ "$touches_ledger" = 1 ] && ! trusted_bot "$sha"; then
|
|
108
195
|
violations=$((violations + 1))
|
|
109
196
|
fi
|
|
@@ -111,7 +198,18 @@ done
|
|
|
111
198
|
|
|
112
199
|
if [ "$violations" -gt 0 ]; then
|
|
113
200
|
echo "FAIL [ledger-guard]: ${violations} commit(s) change CI-owned gate files without a verified gate-bot signature. The ledger is CI-owned — let CI sync the gate; do not commit .sdlc/*.json or reviews/*.md yourself."
|
|
201
|
+
# An epic's seed is exempt only while its ledger is off the base ref. Once the first review PR merges
|
|
202
|
+
# (squashed or rebased, so the SHAs differ), those same seed commits still sitting on a sibling
|
|
203
|
+
# authoring branch read as mutations — the author did nothing wrong and the remedy is a rebase, so
|
|
204
|
+
# name it rather than leaving them with "do not commit the ledger yourself".
|
|
205
|
+
echo "hint [ledger-guard]: if these are seed commits from an already-merged review PR, rebase this branch onto the updated ${BASE} — the seed carve-out applies only until the ledger is on the base ref."
|
|
114
206
|
exit 1
|
|
115
207
|
fi
|
|
116
|
-
|
|
208
|
+
# Say WHICH rule passed the range: claiming "every change is a bot commit" would be false on a range
|
|
209
|
+
# whose only ledger change was a human seed the carve-out let through.
|
|
210
|
+
if [ "${#noted_slugs[@]}" -gt 1 ]; then # >1: the sentinel element is always there
|
|
211
|
+
echo "PASS [ledger-guard]: every CI-owned gate change in ${RANGE} is a verified gate-bot commit or a new epic's seed."
|
|
212
|
+
else
|
|
213
|
+
echo "PASS [ledger-guard]: every CI-owned gate change in ${RANGE} is a verified gate-bot commit."
|
|
214
|
+
fi
|
|
117
215
|
exit 0
|
|
@@ -10,7 +10,30 @@
|
|
|
10
10
|
# best-effort: the commit PASSes with a note (spec-link already proved the story link).
|
|
11
11
|
set -euo pipefail
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
14
|
+
# --- is duplicated, not sourced) ---
|
|
15
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
16
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
17
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
18
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
19
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
20
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
21
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
22
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
23
|
+
# command substitution in an assignment aborts the script.
|
|
24
|
+
resolve_base() {
|
|
25
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
26
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
27
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
28
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
29
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
30
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
31
|
+
done
|
|
32
|
+
printf '%s' origin/main
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
36
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [lineage-check]: no base given — diffing against '${BASE}'."
|
|
14
37
|
|
|
15
38
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
16
39
|
echo "FAIL [lineage-check]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
@@ -10,7 +10,30 @@
|
|
|
10
10
|
# ci/chore/build/test exempt. Fails CLOSED on an unresolvable base.
|
|
11
11
|
set -euo pipefail
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
14
|
+
# --- is duplicated, not sourced) ---
|
|
15
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
16
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
17
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
18
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
19
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
20
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
21
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
22
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
23
|
+
# command substitution in an assignment aborts the script.
|
|
24
|
+
resolve_base() {
|
|
25
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
26
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
27
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
28
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
29
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
30
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
31
|
+
done
|
|
32
|
+
printf '%s' origin/main
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
36
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [reconcile-debt]: no base given — diffing against '${BASE}'."
|
|
14
37
|
|
|
15
38
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
16
39
|
echo "FAIL [reconcile-debt]: base ref '${BASE}' not found — fetch full history / check the base branch."
|
|
@@ -10,7 +10,30 @@
|
|
|
10
10
|
# range), so the report names every offending commit.
|
|
11
11
|
set -euo pipefail
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
14
|
+
# --- is duplicated, not sourced) ---
|
|
15
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
16
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
17
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
18
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
19
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
20
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
21
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
22
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
23
|
+
# command substitution in an assignment aborts the script.
|
|
24
|
+
resolve_base() {
|
|
25
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
26
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
27
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
28
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
29
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
30
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
31
|
+
done
|
|
32
|
+
printf '%s' origin/main
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
36
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [spec-link]: no base given — diffing against '${BASE}'."
|
|
14
37
|
|
|
15
38
|
# Fail closed if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
|
|
16
39
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
@@ -26,7 +26,30 @@
|
|
|
26
26
|
# GITLAB_TOKEN (or SDLC_API_TOKEN) with read_api scope; see the pipeline fragment header.
|
|
27
27
|
set -euo pipefail
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
30
|
+
# --- is duplicated, not sourced) ---
|
|
31
|
+
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
32
|
+
# whose trunk is `develop`/`master` that guess either fails closed or, where a stale `main` still
|
|
33
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/hubcommit.mjs,
|
|
34
|
+
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
35
|
+
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
36
|
+
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
37
|
+
# candidate instead of failing the gate on a fully-fetched repo. CI always passes the base explicitly,
|
|
38
|
+
# so this governs local runs only. The `|| _x=""` guards are load-bearing: under `set -e` a failing
|
|
39
|
+
# command substitution in an assignment aborts the script.
|
|
40
|
+
resolve_base() {
|
|
41
|
+
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
42
|
+
_cfg="$(tr -d '\n' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
43
|
+
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
44
|
+
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
45
|
+
case "$_c" in ''|origin/) continue ;; esac
|
|
46
|
+
if git rev-parse --verify --quiet "${_c}^{commit}" >/dev/null 2>&1; then printf '%s' "$_c"; return; fi
|
|
47
|
+
done
|
|
48
|
+
printf '%s' origin/main
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
BASE="${1:-${SDLC_BASE:-$(resolve_base)}}"
|
|
52
|
+
[ -n "${1:-}" ] || [ -n "${SDLC_BASE:-}" ] || echo "note [verified-commits]: no base given — diffing against '${BASE}'."
|
|
30
53
|
|
|
31
54
|
# Fail closed if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
|
|
32
55
|
if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
|
|
@@ -55,7 +55,9 @@ jobs:
|
|
|
55
55
|
body="$(mktemp)"; printf '%s' "$PR_BODY" > "$body"
|
|
56
56
|
bash checks/pr-template.sh --profile hub --head "$PR_HEAD" --changed "$changed" "$body"
|
|
57
57
|
|
|
58
|
-
# The gate ledger is CI-owned: reject non-bot commits to .sdlc
|
|
58
|
+
# The gate ledger is CI-owned: reject non-bot commits to .sdlc/{state,approvals,comments,hub-prs}.json
|
|
59
|
+
# or reviews/*.md (.sdlc/contract-lock.json is artifact-side and allowed). The one other exception is a
|
|
60
|
+
# brand-new epic's seed, which no CI path can write, so it rides this first review PR (#162).
|
|
59
61
|
ledger-guard:
|
|
60
62
|
runs-on: ubuntu-latest
|
|
61
63
|
if: github.event.action != 'edited'
|
|
@@ -44,7 +44,9 @@ yad-hub-pr-template:
|
|
|
44
44
|
- body="$(mktemp)"; printf '%s' "$CI_MERGE_REQUEST_DESCRIPTION" > "$body"
|
|
45
45
|
- bash checks/pr-template.sh --profile hub --head "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --changed "$changed" "$body"
|
|
46
46
|
|
|
47
|
-
# The gate ledger is CI-owned: reject non-bot commits to .sdlc
|
|
47
|
+
# The gate ledger is CI-owned: reject non-bot commits to .sdlc/{state,approvals,comments,hub-prs}.json
|
|
48
|
+
# or reviews/*.md (.sdlc/contract-lock.json is artifact-side and allowed). The one other exception is a
|
|
49
|
+
# brand-new epic's seed, which no CI path can write, so it rides this first review MR (#162).
|
|
48
50
|
yad-hub-ledger-guard:
|
|
49
51
|
extends: .yad_hub_mr_only
|
|
50
52
|
needs: []
|
|
@@ -112,6 +112,10 @@ Notes:
|
|
|
112
112
|
never escalates to domain owners (no contract surface is touched yet).
|
|
113
113
|
- Also create an empty approvals ledger `.sdlc/approvals.json` and comments ledger
|
|
114
114
|
`.sdlc/comments.json`, each containing `[]`, and the `reviews/` directory.
|
|
115
|
+
- Commit the seed on the `discovery/EP-discovery` branch, and cut `review/EP-discovery/discovery` from
|
|
116
|
+
it so the **first** review PR/MR carries the ledger to the default branch. In bridge mode
|
|
117
|
+
`ledger-guard` exempts a new epic's ledger (creation, not mutation, #162); every later change to it
|
|
118
|
+
is CI's. See `../yad-epic/references/state-schema.md`, "Authoring branches".
|
|
115
119
|
|
|
116
120
|
### Step 6 — Stop at the gate (do NOT advance)
|
|
117
121
|
Report: the path to the discovery set, and that the next action is **review** via `yad-review-gate`
|
|
@@ -2382,9 +2382,9 @@
|
|
|
2382
2382
|
"license": "MIT"
|
|
2383
2383
|
},
|
|
2384
2384
|
"node_modules/js-yaml": {
|
|
2385
|
-
"version": "4.3.
|
|
2386
|
-
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.
|
|
2387
|
-
"integrity": "sha512-
|
|
2385
|
+
"version": "4.3.1",
|
|
2386
|
+
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
|
2387
|
+
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
|
2388
2388
|
"dev": true,
|
|
2389
2389
|
"funding": [
|
|
2390
2390
|
{
|
package/skills/yad-epic/SKILL.md
CHANGED
|
@@ -152,6 +152,10 @@ Notes:
|
|
|
152
152
|
- `test-cases` / `test-cases-review` are a **parallel, non-blocking track**: they seed `blocked` and open
|
|
153
153
|
when `stories-review` passes — at which point the epic is already `ready-for-build`, so the build half
|
|
154
154
|
runs alongside the tester. They never gate `ready-for-build` (see `references/state-schema.md`).
|
|
155
|
+
- Commit the seed on this step's authoring branch. It reaches the hub's default branch through the
|
|
156
|
+
epic's **first** review PR/MR — cut `review/EP-<slug>/epic` from the authoring branch so it carries
|
|
157
|
+
the seed. In bridge mode `ledger-guard` exempts a new epic's ledger (creation, not mutation, #162);
|
|
158
|
+
every later change to it is CI's. See `references/state-schema.md`, "Authoring branches".
|
|
155
159
|
- Also create an empty approvals ledger `{project-root}/epics/EP-<slug>/.sdlc/approvals.json`
|
|
156
160
|
and an empty comments ledger `{project-root}/epics/EP-<slug>/.sdlc/comments.json`, each containing
|
|
157
161
|
`[]`, and the `reviews/` directory. (`comments.json` is the machine-readable counterpart to the
|
|
@@ -94,6 +94,16 @@ The shared procedure (run once the `EP-<slug>` is known):
|
|
|
94
94
|
hub's default branch (`git checkout -b <step>/EP-<slug>`).
|
|
95
95
|
3. Author and commit the step's artifact(s) on that branch. The bridge's `review/…` branch is created
|
|
96
96
|
separately at review time and is untouched by this step.
|
|
97
|
+
|
|
98
|
+
**How the seed reaches the default branch.** The `.sdlc/` ledger is seeded once, by hand, on the
|
|
99
|
+
**entry** step's authoring branch (`analysis/…`, `epic/…`, `change/…`, `discovery/…`) — no CLI or CI
|
|
100
|
+
path creates one (`yad gate ci` only *advances* an existing chain, at merge, on the default branch).
|
|
101
|
+
So for the **first** gate of an epic, cut `review/EP-<slug>/<artifact-base>` from that authoring
|
|
102
|
+
branch: the review PR/MR then carries the seed alongside the artifact, and the ledger lands on the
|
|
103
|
+
default branch when it merges. In bridge mode `ledger-guard` exempts exactly this case — **creation,
|
|
104
|
+
not mutation** (#162) — so no direct push to a protected default branch is needed. For every **later**
|
|
105
|
+
gate the ledger is already on the default branch: cut the review branch from there, commit the
|
|
106
|
+
artifact only, and leave `.sdlc/{state,approvals,comments,hub-prs}.json` and `reviews/*.md` to CI.
|
|
97
107
|
| `type` | `author` \| `review+approve` | Authoring step or a team review gate. |
|
|
98
108
|
| `artifact` | filename or folder | The file/folder this step produces or gates. |
|
|
99
109
|
| `assistance` | `none` \| `review` \| `heavy` | Dial 1 — how much AI helps (build plan §2). |
|
|
@@ -52,7 +52,10 @@ each required domain-owner to a platform `login` via the roster (a roster `name`
|
|
|
52
52
|
### Step 2 — `open` (create the review PR/MR)
|
|
53
53
|
1. From the hub default branch, create `review/EP-<slug>/<artifact-base>` and ensure the artifact file
|
|
54
54
|
(and, for architecture, `contract.md` + `.sdlc/contract-lock.json`) is committed on it. Push as the
|
|
55
|
-
local user.
|
|
55
|
+
local user. **First gate of a new epic:** cut the review branch from the **authoring** branch
|
|
56
|
+
(`epic/…`, `change/…`, `analysis/…`, `discovery/…`) instead, so it carries the `.sdlc/` **seed** —
|
|
57
|
+
that PR/MR is the only way a new epic's ledger reaches the default branch, and `ledger-guard`
|
|
58
|
+
exempts it (see step 4).
|
|
56
59
|
2. Open the PR/MR with `gh`/`glab` using the hub body template (`yad-pr-template` `templates/hub/…`),
|
|
57
60
|
filled with the epic, artifact, gate step, owner, `epic.repos`, and the step's risk tags.
|
|
58
61
|
3. **Request the required reviewers** (their logins) and add a `domain:<repo>` label per touched repo so
|
|
@@ -68,7 +71,9 @@ each required domain-owner to a platform `login` via the roster (a roster `name`
|
|
|
68
71
|
A human commit touching the gate-state files (`.sdlc/{state,approvals,comments,hub-prs}.json` or
|
|
69
72
|
`reviews/*.md`; `.sdlc/contract-lock.json` is artifact-side and allowed) on a review PR is rejected
|
|
70
73
|
by the `ledger-guard` check. (The `yad gate open` CLI behaves the same: in bridge mode it opens the
|
|
71
|
-
PR only and writes no ledger.)
|
|
74
|
+
PR only and writes no ledger.) The **one** exception is a brand-new epic's **seed** — no CI path can
|
|
75
|
+
create a ledger, so an epic whose `.sdlc/state.json` is absent from the base ref may be created by a
|
|
76
|
+
human on this first PR/MR (#162). Every later change to it is CI's alone.
|
|
72
77
|
5. Report the PR/MR URL and the required reviewers. **Do not** record approvals or advance — reviewers
|
|
73
78
|
act on the platform; CI (`yad gate ci`) reconciles it onto the default branch at merge.
|
|
74
79
|
|
|
@@ -143,7 +148,8 @@ default branch. (File-only mode keeps `yad gate sync` as the local writer.)
|
|
|
143
148
|
- **Protect the hub default branch.** Require that `epics/**` artifacts change only through a review
|
|
144
149
|
PR/MR (branch protection). This keeps revoke-on-change sound: it removes the only window where a
|
|
145
150
|
delayed reconcile could advance on an out-of-band post-merge artifact change (see `references/bridge.md`,
|
|
146
|
-
"Known limitation").
|
|
151
|
+
"Known limitation"). Safe to require: a new epic's `.sdlc/` seed rides its first review PR/MR, so
|
|
152
|
+
nobody needs a direct push to the default branch to start an epic (#162).
|
|
147
153
|
- **Degrade gracefully.** No platform / disabled bridge / no CLI → the gate runs file-only with no error.
|
|
148
154
|
|
|
149
155
|
## Reference
|
|
@@ -155,7 +155,9 @@ During review CI writes nothing: the platform PR/MR is the source of truth (nati
|
|
|
155
155
|
threads). The CLI is self-sufficient at merge: it derives the epic + artifact from the
|
|
156
156
|
`review/EP-<slug>/<artifact-base>` head branch, takes the PR/MR number from the event (GitHub) or
|
|
157
157
|
resolves it from the platform (GitLab), upserts the `hub-prs.json` entry itself, and **re-reads
|
|
158
|
-
approvals fresh from the platform** — so no ledger needs to be pre-seeded on the branch.
|
|
158
|
+
approvals fresh from the platform** — so no ledger needs to be pre-seeded on the branch. (It only
|
|
159
|
+
*advances* a chain, though: it cannot **create** one. A brand-new epic's seed therefore travels the
|
|
160
|
+
other way — up through its first review PR/MR; see "the seed of a new epic" below.)
|
|
159
161
|
|
|
160
162
|
| Platform event | Phase | CI action |
|
|
161
163
|
|---|---|---|
|
|
@@ -174,13 +176,26 @@ commit — the advance plus the `draft → approved` status flip — lands on th
|
|
|
174
176
|
check (yad-checks) FAILs any commit on a review PR that touches `.sdlc/{state,approvals,comments,hub-prs}
|
|
175
177
|
.json` or `reviews/*.md` (`.sdlc/contract-lock.json` is artifact-side and allowed). Under Path B **no
|
|
176
178
|
CI commit lands in a review PR at all**, so the only ledger change the guard can see there is a human
|
|
177
|
-
edit — which it rejects
|
|
179
|
+
edit — which it rejects, with one carve-out for a new epic's seed (below). (The `verified-commits`
|
|
180
|
+
gate still vets every commit's signature + author;
|
|
178
181
|
its gate-bot exemption is now vestigial in-PR because CI no longer commits there.) `yad gate open`
|
|
179
182
|
opens the PR only; local `yad gate sync` is advisory in bridge mode (writes nothing). After a merge,
|
|
180
183
|
everyone `git checkout <default> && git pull`. (Without the bridge, humans own the ledger locally and
|
|
181
184
|
these guards are no-ops.)
|
|
182
185
|
|
|
183
|
-
**The one sanctioned human ledger write
|
|
186
|
+
**The one sanctioned human ledger write *in a review PR*: the seed of a new epic.** `gate ci` only
|
|
187
|
+
**advances** an existing chain — it bails on a missing `state.json`, and the engine reads that absence
|
|
188
|
+
as "not seeded yet" — so no CI path can ever create a ledger. The seed the authoring skills write
|
|
189
|
+
(`yad-epic`, `yad-change`, `yad-analysis`, `yad-discovery`, `yad-stub`) can reach the default branch
|
|
190
|
+
only through the epic's **first** review PR/MR, which is exactly where `ledger-guard` runs. So the gate
|
|
191
|
+
exempts **creation**: an epic whose `.sdlc/state.json` is absent from the PR's base ref may have its
|
|
192
|
+
ledger written by a human there (#162). It stays a narrow carve-out — the probe is against the base
|
|
193
|
+
ref, not the parent commit, so deleting an on-trunk `state.json` to "re-seed" it is itself a rejected
|
|
194
|
+
mutation, and the moment the ledger is on the default branch every further change is CI's alone. Cut
|
|
195
|
+
that first review branch from the authoring branch (`epic/…`, `change/…`) so it carries the seed; no
|
|
196
|
+
direct push to a protected default branch is needed.
|
|
197
|
+
|
|
198
|
+
**The one sanctioned human ledger write *on the default branch*: `yad gate repair`.** It heals a `YAD-STATE-005` chain (an
|
|
184
199
|
authoring step stranded behind a review gate that already advanced) by writing `state.json` alone. This
|
|
185
200
|
is not a `ledger-guard` gap: the repair commits to the **default branch**, where `ledger-guard` — which
|
|
186
201
|
only inspects review PRs — never runs, and where the `yad-update-guard` (platform-Verified signature +
|
|
@@ -176,7 +176,9 @@ PR only — against the `review/<epic>/<artifact>` branch, which must already ex
|
|
|
176
176
|
records this skill describes. The skill's
|
|
177
177
|
job is the human half: presenting the artifact, helping the owner address comments, and narrating the
|
|
178
178
|
gate. Local `yad gate sync` is advisory in bridge mode (reads the platform, prints status, writes
|
|
179
|
-
nothing); a human must never commit gate-state files (the `ledger-guard` check rejects it).
|
|
179
|
+
nothing); a human must never commit gate-state files (the `ledger-guard` check rejects it). The single
|
|
180
|
+
exception is an epic's **seed** — no CI path can create a ledger, so a brand-new epic's `.sdlc/` rides
|
|
181
|
+
its **first** review PR/MR, cut from the authoring branch (creation, not mutation, #162).
|
|
180
182
|
|
|
181
183
|
Under that CLI the gate **advances on merge**: a review PR/MR whose reviewer rule is satisfied, whose
|
|
182
184
|
comment threads are **all resolved**, and which has been **merged** auto-marks the step `done` and
|
package/skills/yad-stub/SKILL.md
CHANGED
|
@@ -115,6 +115,11 @@ into normal authoring with zero re-seeding.
|
|
|
115
115
|
Also create the empty ledgers `{.sdlc/approvals.json}` and `{.sdlc/comments.json}` (each `[]`) and the
|
|
116
116
|
`reviews/` directory. **Do NOT** write a `contract-lock.json` — a stub has no locked surface yet.
|
|
117
117
|
|
|
118
|
+
Commit the seed on this step's authoring branch; it reaches the hub's default branch through the
|
|
119
|
+
epic's **first** review PR/MR (or, for a stub, the PR that carries the stub itself). In bridge mode
|
|
120
|
+
`ledger-guard` exempts a new epic's ledger — creation, not mutation (#162) — while every later change
|
|
121
|
+
to it stays CI's. See `../yad-epic/references/state-schema.md`, "Authoring branches".
|
|
122
|
+
|
|
118
123
|
### Step 6 — Stop; hand off (NO auto-advance)
|
|
119
124
|
Report the new `EP-<slug>`, that it is a **stub (backfill pending)**, and the two next moves:
|
|
120
125
|
- **File bugs now:** `yad-change` (`--parent EP-<slug>`, `kind: defect|change`) — the defect threads off
|