yadflow 4.0.0-next.3 → 4.0.0
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 +699 -0
- package/README.md +85 -22
- package/bin/commands.mjs +3 -1
- package/bin/yad.mjs +183 -29
- package/cli/capture.mjs +4 -2
- package/cli/checkpoint.mjs +10 -10
- package/cli/claims.mjs +4 -4
- package/cli/commit.mjs +48 -12
- package/cli/docs.mjs +15 -13
- package/cli/doctor.mjs +557 -141
- package/cli/epic-state.mjs +41 -18
- package/cli/errors.mjs +3 -2
- package/cli/find-product.mjs +197 -0
- package/cli/fold.mjs +4 -4
- package/cli/gate-local.mjs +13 -13
- package/cli/gate.mjs +111 -103
- package/cli/history.mjs +13 -13
- package/cli/hook.mjs +201 -30
- package/cli/index-command.mjs +6 -6
- package/cli/lib.mjs +91 -29
- package/cli/manifest.mjs +171 -104
- package/cli/migrate.mjs +208 -17
- package/cli/mode.mjs +22 -22
- package/cli/next.mjs +6 -6
- package/cli/openpr.mjs +32 -24
- package/cli/people.mjs +22 -10
- package/cli/plan.mjs +718 -45
- package/cli/platform.mjs +13 -7
- package/cli/product-index.mjs +4 -4
- package/cli/{hubcommit.mjs → productcommit.mjs} +11 -7
- package/cli/protection.mjs +19 -9
- package/cli/reconcile.mjs +98 -23
- package/cli/repo-publish.mjs +22 -16
- package/cli/repo.mjs +128 -33
- package/cli/report.mjs +4 -2
- package/cli/review.mjs +26 -19
- package/cli/setup.mjs +105 -36
- package/cli/ship.mjs +1 -1
- package/cli/skip.mjs +4 -4
- package/cli/tidy.mjs +7 -7
- package/cli/update-commit.mjs +22 -19
- package/cli/workspace.mjs +495 -0
- package/package.json +7 -4
- package/skills/sdlc/config.yaml +14 -13
- package/skills/sdlc/module-help.csv +8 -8
- package/skills/yad-analysis/SKILL.md +6 -4
- package/skills/yad-architecture/SKILL.md +4 -3
- package/skills/yad-backfill/SKILL.md +11 -5
- package/skills/yad-backfill/references/backfill.md +33 -3
- package/skills/yad-backfill/templates/checks/backfill-check.sh +200 -17
- package/skills/yad-change/SKILL.md +1 -1
- package/skills/yad-checks/SKILL.md +48 -27
- package/skills/yad-checks/references/check-gates.md +213 -53
- package/skills/yad-checks/templates/checks/commit-message.sh +39 -13
- package/skills/yad-checks/templates/checks/contract-check.sh +472 -23
- package/skills/yad-checks/templates/checks/epic-open.sh +398 -8
- package/skills/yad-checks/templates/checks/ledger-guard.sh +75 -25
- package/skills/yad-checks/templates/checks/lineage-check.sh +391 -7
- package/skills/yad-checks/templates/checks/product-checkout.sh +141 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +399 -8
- package/skills/yad-checks/templates/checks/risk-map-check.sh +35 -3
- package/skills/yad-checks/templates/checks/spec-link.sh +36 -3
- package/skills/yad-checks/templates/checks/verified-commits.sh +26 -2
- package/skills/yad-checks/templates/github/yad-checks.yml +33 -13
- package/skills/yad-checks/templates/github/{yad-hub-checks.yml → yad-product-checks.yml} +14 -10
- package/skills/yad-checks/templates/github/yad-update-guard.yml +1 -1
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +1 -1
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +12 -0
- package/skills/yad-checks/templates/gitlab/{yad-hub-checks.gitlab-ci.yml → yad-product-checks.gitlab-ci.yml} +16 -16
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +2 -2
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.mjs +130 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.mjs +117 -0
- package/skills/yad-checks/templates/hooks/yad-capture.mjs +117 -0
- package/skills/yad-commit/SKILL.md +18 -7
- package/skills/yad-connect-design/SKILL.md +1 -1
- package/skills/yad-connect-design/references/design-registry.md +1 -1
- package/skills/yad-connect-docs/SKILL.md +16 -11
- package/skills/yad-connect-docs/references/docs-registry.md +8 -8
- package/skills/yad-connect-learning/SKILL.md +1 -1
- package/skills/yad-connect-learning/references/learning-registry.md +1 -1
- package/skills/yad-connect-repos/SKILL.md +31 -8
- package/skills/yad-connect-repos/references/code-context.md +1 -1
- package/skills/yad-connect-repos/references/{hub-config.md → product-config.md} +19 -11
- package/skills/yad-connect-repos/references/repos-registry.md +1 -1
- package/skills/yad-connect-testing/SKILL.md +1 -1
- package/skills/yad-connect-testing/references/testing-registry.md +1 -1
- package/skills/yad-discovery/SKILL.md +2 -2
- package/skills/yad-discovery/references/discovery-schema.md +1 -1
- package/skills/yad-docs/SKILL.md +1 -1
- package/skills/yad-docs/references/data-mapping.md +1 -1
- package/skills/yad-docs/templates/app/package-lock.json +8 -8
- package/skills/yad-docs/templates/app/package.json +1 -1
- package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +2 -2
- package/skills/yad-docs-overview/references/pipeline-model.md +3 -3
- package/skills/yad-engineer-review/SKILL.md +2 -2
- package/skills/yad-engineer-review/references/ship-and-record.md +3 -3
- package/skills/yad-epic/SKILL.md +6 -4
- package/skills/yad-epic/references/state-schema.md +13 -12
- package/skills/yad-open-pr/SKILL.md +1 -1
- package/skills/yad-pair-review/SKILL.md +5 -5
- package/skills/yad-pair-review/references/session-state.md +2 -2
- package/skills/yad-pr-template/SKILL.md +14 -12
- package/skills/yad-pr-template/templates/checks/pr-template.sh +18 -16
- package/skills/yad-pr-template/templates/checks/pr-title.sh +15 -13
- package/skills/yad-pr-template/templates/{hub → product}/github/pull_request_template.md +2 -2
- package/skills/yad-pr-template/templates/{hub → product}/gitlab/merge_request_templates/Default.md +2 -2
- package/skills/{yad-hub-bridge → yad-product-bridge}/SKILL.md +15 -15
- package/skills/{yad-hub-bridge → yad-product-bridge}/references/bridge.md +15 -10
- package/skills/{yad-hub-bridge → yad-product-bridge}/references/login-roster.md +2 -2
- package/skills/{yad-hub-bridge/templates/checks/hub-route.sh → yad-product-bridge/templates/checks/product-route.sh} +2 -2
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/github/yad-gate-sync.yml +29 -16
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/gitlab-ci.include-root.yml +1 -1
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/yad-gate-sync.gitlab-ci.yml +5 -3
- package/skills/yad-report/SKILL.md +1 -1
- package/skills/yad-review-companion/SKILL.md +3 -3
- package/skills/yad-review-gate/SKILL.md +22 -23
- package/skills/yad-review-gate/references/gating.md +13 -13
- package/skills/yad-run/SKILL.md +2 -2
- package/skills/yad-run/references/run-loop.md +2 -2
- package/skills/yad-spec/SKILL.md +2 -1
- package/skills/yad-spec/references/spec-handoff.md +19 -10
- package/skills/yad-stories/SKILL.md +2 -2
- package/skills/yad-sync-repos/SKILL.md +19 -3
- package/skills/yad-test-cases/SKILL.md +2 -2
- package/skills/yad-ui/SKILL.md +2 -2
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +0 -91
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +0 -100
- package/skills/yad-checks/templates/hooks/yad-capture.sh +0 -53
|
@@ -25,11 +25,35 @@
|
|
|
25
25
|
# GITLAB_TOKEN (or SDLC_API_TOKEN) with read_api scope; see the pipeline fragment header.
|
|
26
26
|
set -euo pipefail
|
|
27
27
|
|
|
28
|
+
# --- shared Product settings file (byte-identical across the gates; they are standalone by design, so
|
|
29
|
+
# --- it is duplicated, not sourced) ---
|
|
30
|
+
# The Product's settings live under two names until v5: `.sdlc/product.json`, read first from 4.0, and
|
|
31
|
+
# `.sdlc/hub.json`, which an older yadflow wrote and its gates read. The environment may name the file
|
|
32
|
+
# instead: SDLC_PRODUCT_CONFIG, or the older SDLC_HUB_CONFIG. Two names that say different things are
|
|
33
|
+
# never settled by picking one — the CLI refuses the same way (YAD-STATE-008) — so the gate FAILS and
|
|
34
|
+
# names both. Said on stderr and returned as a failure: this runs inside `$(...)`, where an `exit` would
|
|
35
|
+
# only leave the subshell. `cmp -s` is byte for byte, like the CLI's own comparison.
|
|
36
|
+
product_config() {
|
|
37
|
+
if [ -n "${SDLC_PRODUCT_CONFIG:-}" ] && [ -n "${SDLC_HUB_CONFIG:-}" ] \
|
|
38
|
+
&& [ "$SDLC_PRODUCT_CONFIG" != "$SDLC_HUB_CONFIG" ] && ! cmp -s "$SDLC_PRODUCT_CONFIG" "$SDLC_HUB_CONFIG"; then
|
|
39
|
+
echo "FAIL [product-settings]: SDLC_PRODUCT_CONFIG (${SDLC_PRODUCT_CONFIG}) and SDLC_HUB_CONFIG (${SDLC_HUB_CONFIG}) name files that say different things. Set only SDLC_PRODUCT_CONFIG." >&2
|
|
40
|
+
return 1
|
|
41
|
+
fi
|
|
42
|
+
if [ -n "${SDLC_PRODUCT_CONFIG:-}" ]; then printf '%s' "$SDLC_PRODUCT_CONFIG"; return 0; fi
|
|
43
|
+
if [ -n "${SDLC_HUB_CONFIG:-}" ]; then printf '%s' "$SDLC_HUB_CONFIG"; return 0; fi
|
|
44
|
+
if [ -f .sdlc/product.json ] && [ -f .sdlc/hub.json ] && ! cmp -s .sdlc/product.json .sdlc/hub.json; then
|
|
45
|
+
echo "FAIL [product-settings]: .sdlc/product.json and .sdlc/hub.json say different things — they are one file under two names until v5. Run \`yad migrate\` in the Product to choose the copy to keep, and commit both." >&2
|
|
46
|
+
return 1
|
|
47
|
+
fi
|
|
48
|
+
if [ -f .sdlc/product.json ]; then printf '%s' .sdlc/product.json; else printf '%s' .sdlc/hub.json; fi
|
|
49
|
+
}
|
|
50
|
+
PRODUCT_CONFIG="$(product_config)" || exit 1
|
|
51
|
+
|
|
28
52
|
# --- shared base resolution (byte-identical across the gates; they are standalone by design, so it
|
|
29
53
|
# --- is duplicated, not sourced) ---
|
|
30
54
|
# With no explicit base, RESOLVE the trunk instead of assuming a hardcoded `origin/main` — on a repo
|
|
31
55
|
# 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/
|
|
56
|
+
# exists, silently diffs the WRONG range (issue #161). Mirrors the CLI's own order (cli/productcommit.mjs,
|
|
33
57
|
# cli/repo.mjs): the CONFIGURED default_branch first, then the remote's published default
|
|
34
58
|
# (origin/HEAD), then origin/main. Each candidate must actually resolve before it is used, so a
|
|
35
59
|
# DANGLING origin/HEAD (trunk renamed, the old remote-tracking ref pruned) falls through to the next
|
|
@@ -38,7 +62,7 @@ set -euo pipefail
|
|
|
38
62
|
# command substitution in an assignment aborts the script.
|
|
39
63
|
resolve_base() {
|
|
40
64
|
# tr first: a key and its value may legally sit on separate lines, which a per-line match misses.
|
|
41
|
-
_cfg="$(tr -d '\n' < "$
|
|
65
|
+
_cfg="$({ tr -d '\n' < "$PRODUCT_CONFIG"; } 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
|
|
42
66
|
_head="$(git symbolic-ref --short --quiet refs/remotes/origin/HEAD 2>/dev/null)" || _head=""
|
|
43
67
|
for _c in "origin/${_cfg}" "${_head}" origin/main; do
|
|
44
68
|
case "$_c" in ''|origin/) continue ;; esac
|
|
@@ -12,12 +12,16 @@ on:
|
|
|
12
12
|
types: [opened, synchronize, reopened, edited]
|
|
13
13
|
branches: ["**"]
|
|
14
14
|
|
|
15
|
+
# Read-only by default: no gate here writes to the repo. A job that needs more asks for it itself.
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
15
19
|
jobs:
|
|
16
20
|
spec-link:
|
|
17
21
|
runs-on: ubuntu-latest
|
|
18
22
|
if: github.event.action != 'edited'
|
|
19
23
|
steps:
|
|
20
|
-
- uses: actions/checkout@v4
|
|
24
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
21
25
|
with: { fetch-depth: 0 }
|
|
22
26
|
- run: bash checks/spec-link.sh "origin/${{ github.base_ref }}"
|
|
23
27
|
|
|
@@ -25,8 +29,12 @@ jobs:
|
|
|
25
29
|
runs-on: ubuntu-latest
|
|
26
30
|
if: github.event.action != 'edited'
|
|
27
31
|
steps:
|
|
28
|
-
- uses: actions/checkout@v4
|
|
32
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
29
33
|
with: { fetch-depth: 0 }
|
|
34
|
+
# E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN
|
|
35
|
+
# secret is set; without it this prints a note and the gate defers, as before.
|
|
36
|
+
- run: bash checks/product-checkout.sh "origin/${{ github.base_ref }}"
|
|
37
|
+
env: { YAD_PRODUCT_TOKEN: "${{ secrets.YAD_PRODUCT_TOKEN }}" }
|
|
30
38
|
- run: bash checks/contract-check.sh "origin/${{ github.base_ref }}"
|
|
31
39
|
|
|
32
40
|
# E65/E66 — the risk map (.sdlc/risk-map: a risk level per directory, no names). Warnings, and the
|
|
@@ -36,7 +44,7 @@ jobs:
|
|
|
36
44
|
runs-on: ubuntu-latest
|
|
37
45
|
if: github.event.action != 'edited'
|
|
38
46
|
steps:
|
|
39
|
-
- uses: actions/checkout@v4
|
|
47
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
40
48
|
with: { fetch-depth: 0 }
|
|
41
49
|
- run: bash checks/risk-map-check.sh "origin/${{ github.base_ref }}"
|
|
42
50
|
|
|
@@ -50,11 +58,11 @@ jobs:
|
|
|
50
58
|
NX_HEAD: ${{ github.event.pull_request.head.sha }}
|
|
51
59
|
COREPACK_HOME: ${{ github.workspace }}/.corepack-cache # pinned-manager downloads, cached below
|
|
52
60
|
steps:
|
|
53
|
-
- uses: actions/checkout@v4
|
|
61
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
54
62
|
with:
|
|
55
63
|
fetch-depth: 0
|
|
56
64
|
filter: blob:none
|
|
57
|
-
- uses: actions/setup-node@v4
|
|
65
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
58
66
|
with:
|
|
59
67
|
node-version: ${{ env.YAD_NODE_VERSION }}
|
|
60
68
|
# Dependency cache for either manager: npm's ~/.npm, pnpm's content-addressable store, and the
|
|
@@ -63,7 +71,7 @@ jobs:
|
|
|
63
71
|
# package.json, since a packageManager bump changes no lockfile. setup-node's own `cache:` must
|
|
64
72
|
# name the manager up front (and needs pnpm on PATH before it runs), which package.json only
|
|
65
73
|
# decides later — so cache the directories directly.
|
|
66
|
-
- uses: actions/cache@v4
|
|
74
|
+
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
67
75
|
with:
|
|
68
76
|
path: |
|
|
69
77
|
~/.npm
|
|
@@ -82,24 +90,36 @@ jobs:
|
|
|
82
90
|
runs-on: ubuntu-latest
|
|
83
91
|
if: github.event.action != 'edited'
|
|
84
92
|
steps:
|
|
85
|
-
- uses: actions/checkout@v4
|
|
93
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
86
94
|
with: { fetch-depth: 0 }
|
|
95
|
+
# E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN
|
|
96
|
+
# secret is set; without it this prints a note and the gate defers, as before.
|
|
97
|
+
- run: bash checks/product-checkout.sh "origin/${{ github.base_ref }}"
|
|
98
|
+
env: { YAD_PRODUCT_TOKEN: "${{ secrets.YAD_PRODUCT_TOKEN }}" }
|
|
87
99
|
- run: bash checks/lineage-check.sh "origin/${{ github.base_ref }}"
|
|
88
100
|
|
|
89
101
|
epic-open:
|
|
90
102
|
runs-on: ubuntu-latest
|
|
91
103
|
if: github.event.action != 'edited'
|
|
92
104
|
steps:
|
|
93
|
-
- uses: actions/checkout@v4
|
|
105
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
94
106
|
with: { fetch-depth: 0 }
|
|
107
|
+
# E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN
|
|
108
|
+
# secret is set; without it this prints a note and the gate defers, as before.
|
|
109
|
+
- run: bash checks/product-checkout.sh "origin/${{ github.base_ref }}"
|
|
110
|
+
env: { YAD_PRODUCT_TOKEN: "${{ secrets.YAD_PRODUCT_TOKEN }}" }
|
|
95
111
|
- run: bash checks/epic-open.sh "origin/${{ github.base_ref }}"
|
|
96
112
|
|
|
97
113
|
reconcile-debt:
|
|
98
114
|
runs-on: ubuntu-latest
|
|
99
115
|
if: github.event.action != 'edited'
|
|
100
116
|
steps:
|
|
101
|
-
- uses: actions/checkout@v4
|
|
117
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
102
118
|
with: { fetch-depth: 0 }
|
|
119
|
+
# E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN
|
|
120
|
+
# secret is set; without it this prints a note and the gate defers, as before.
|
|
121
|
+
- run: bash checks/product-checkout.sh "origin/${{ github.base_ref }}"
|
|
122
|
+
env: { YAD_PRODUCT_TOKEN: "${{ secrets.YAD_PRODUCT_TOKEN }}" }
|
|
103
123
|
- run: bash checks/reconcile-debt-check.sh "origin/${{ github.base_ref }}"
|
|
104
124
|
|
|
105
125
|
# Pattern gates: commit subject + PR title + PR body all follow the convention (profile: code).
|
|
@@ -108,7 +128,7 @@ jobs:
|
|
|
108
128
|
runs-on: ubuntu-latest
|
|
109
129
|
if: github.event.action != 'edited'
|
|
110
130
|
steps:
|
|
111
|
-
- uses: actions/checkout@v4
|
|
131
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
112
132
|
with: { fetch-depth: 0 }
|
|
113
133
|
- run: bash checks/commit-message.sh --profile code "origin/${{ github.base_ref }}"
|
|
114
134
|
|
|
@@ -118,7 +138,7 @@ jobs:
|
|
|
118
138
|
env:
|
|
119
139
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
120
140
|
steps:
|
|
121
|
-
- uses: actions/checkout@v4
|
|
141
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
122
142
|
- run: bash checks/pr-title.sh --profile code "$PR_TITLE"
|
|
123
143
|
|
|
124
144
|
pr-template:
|
|
@@ -126,7 +146,7 @@ jobs:
|
|
|
126
146
|
env:
|
|
127
147
|
PR_BODY: ${{ github.event.pull_request.body }}
|
|
128
148
|
steps:
|
|
129
|
-
- uses: actions/checkout@v4
|
|
149
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
130
150
|
- run: |
|
|
131
151
|
body="$(mktemp)"; printf '%s' "$PR_BODY" > "$body"
|
|
132
152
|
bash checks/pr-template.sh --profile code "$body"
|
|
@@ -141,6 +161,6 @@ jobs:
|
|
|
141
161
|
env:
|
|
142
162
|
GH_TOKEN: ${{ github.token }} # read-only: gh api commits/<sha> for the Verified badge
|
|
143
163
|
steps:
|
|
144
|
-
- uses: actions/checkout@v4
|
|
164
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
145
165
|
with: { fetch-depth: 0 }
|
|
146
166
|
- run: bash checks/verified-commits.sh "origin/${{ github.base_ref }}"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# yad-managed: yad-checks
|
|
2
|
-
# Pattern gates for the PRODUCT. They run with `--profile
|
|
2
|
+
# Pattern gates for the PRODUCT. They run with `--profile product` and split by head branch:
|
|
3
3
|
# review/EP-* PRs are Shape artifact-review vehicles — a `review: <artifact> (EP-<slug>)` title
|
|
4
4
|
# and the Product artifact-review template body.
|
|
5
5
|
# every other PR is a tooling/code change to the Product itself — a Conventional-Commits title and the
|
|
6
6
|
# code task template body (same convention the code repos use).
|
|
7
7
|
# The head ref is passed via --head so the gate can tell the two apart. Commit subjects are always
|
|
8
8
|
# Conventional-Commits. Standalone workflow so it never collides with the code-repo yad-checks workflow.
|
|
9
|
-
name: yad-
|
|
9
|
+
name: yad-product-checks
|
|
10
10
|
on:
|
|
11
11
|
pull_request:
|
|
12
12
|
# `edited` (beyond the opened/synchronize/reopened defaults) so a PR title/body correction
|
|
@@ -14,6 +14,10 @@ on:
|
|
|
14
14
|
types: [opened, synchronize, reopened, edited]
|
|
15
15
|
branches: ["**"]
|
|
16
16
|
|
|
17
|
+
# Read-only by default: no gate here writes to the repo. A job that needs more asks for it itself.
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
17
21
|
jobs:
|
|
18
22
|
# commit-message + ledger-guard read the commit range, not the PR title/body — skip them on a bare
|
|
19
23
|
# `edited` event (only pr-title/pr-template need to re-check a title/body fix).
|
|
@@ -21,9 +25,9 @@ jobs:
|
|
|
21
25
|
runs-on: ubuntu-latest
|
|
22
26
|
if: github.event.action != 'edited'
|
|
23
27
|
steps:
|
|
24
|
-
- uses: actions/checkout@v4
|
|
28
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
25
29
|
with: { fetch-depth: 0 }
|
|
26
|
-
- run: bash checks/commit-message.sh --profile
|
|
30
|
+
- run: bash checks/commit-message.sh --profile product "origin/${{ github.base_ref }}"
|
|
27
31
|
|
|
28
32
|
# Pass the title via env (never interpolate untrusted ${{ }} into a run line — injection-safe).
|
|
29
33
|
# fetch-depth: 0 + the base ref give us the PR's changed paths, so the gate can reject an artifact
|
|
@@ -35,11 +39,11 @@ jobs:
|
|
|
35
39
|
PR_HEAD: ${{ github.event.pull_request.head.ref }}
|
|
36
40
|
BASE_REF: ${{ github.base_ref }}
|
|
37
41
|
steps:
|
|
38
|
-
- uses: actions/checkout@v4
|
|
42
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
39
43
|
with: { fetch-depth: 0 }
|
|
40
44
|
- run: |
|
|
41
45
|
changed="$(mktemp)"; git -c core.quotePath=false diff --no-renames --name-only "origin/${BASE_REF}...HEAD" > "$changed"
|
|
42
|
-
bash checks/pr-title.sh --profile
|
|
46
|
+
bash checks/pr-title.sh --profile product --head "$PR_HEAD" --changed "$changed" "$PR_TITLE"
|
|
43
47
|
|
|
44
48
|
pr-template:
|
|
45
49
|
runs-on: ubuntu-latest
|
|
@@ -48,20 +52,20 @@ jobs:
|
|
|
48
52
|
PR_HEAD: ${{ github.event.pull_request.head.ref }}
|
|
49
53
|
BASE_REF: ${{ github.base_ref }}
|
|
50
54
|
steps:
|
|
51
|
-
- uses: actions/checkout@v4
|
|
55
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
52
56
|
with: { fetch-depth: 0 }
|
|
53
57
|
- run: |
|
|
54
58
|
changed="$(mktemp)"; git -c core.quotePath=false diff --no-renames --name-only "origin/${BASE_REF}...HEAD" > "$changed"
|
|
55
59
|
body="$(mktemp)"; printf '%s' "$PR_BODY" > "$body"
|
|
56
|
-
bash checks/pr-template.sh --profile
|
|
60
|
+
bash checks/pr-template.sh --profile product --head "$PR_HEAD" --changed "$changed" "$body"
|
|
57
61
|
|
|
58
|
-
# The gate ledger is CI-owned: reject non-bot commits to .sdlc/{state,approvals,comments,hub-prs}.json
|
|
62
|
+
# The gate ledger is CI-owned: reject non-bot commits to .sdlc/{state,approvals,comments,product-prs,hub-prs}.json
|
|
59
63
|
# or reviews/*.md (.sdlc/contract-lock.json is artifact-side and allowed). The one other exception is a
|
|
60
64
|
# brand-new epic's seed, which no CI path can write, so it rides this first review PR (#162).
|
|
61
65
|
ledger-guard:
|
|
62
66
|
runs-on: ubuntu-latest
|
|
63
67
|
if: github.event.action != 'edited'
|
|
64
68
|
steps:
|
|
65
|
-
- uses: actions/checkout@v4
|
|
69
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
66
70
|
with: { fetch-depth: 0 }
|
|
67
71
|
- run: bash checks/ledger-guard.sh "origin/${{ github.base_ref }}"
|
|
@@ -30,7 +30,7 @@ jobs:
|
|
|
30
30
|
env:
|
|
31
31
|
GH_TOKEN: ${{ github.token }} # read-only: gh api commits/<sha> for the Verified badge
|
|
32
32
|
steps:
|
|
33
|
-
- uses: actions/checkout@v4
|
|
33
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
34
34
|
with: { fetch-depth: 0 }
|
|
35
35
|
- name: Resolve the pushed range base
|
|
36
36
|
id: base
|
|
@@ -17,6 +17,6 @@ jobs:
|
|
|
17
17
|
env:
|
|
18
18
|
GH_TOKEN: ${{ github.token }} # read-only: gh api commits/<sha> for the Verified badge
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
21
21
|
with: { fetch-depth: 0 }
|
|
22
22
|
- run: bash checks/verified-commits.sh "origin/${{ github.base_ref }}"
|
|
@@ -51,6 +51,9 @@ yad-contract-check:
|
|
|
51
51
|
extends: .sdlc_mr_only
|
|
52
52
|
needs: []
|
|
53
53
|
script:
|
|
54
|
+
# E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN CI/CD
|
|
55
|
+
# variable is set (masked); without it this prints a note and the gate defers, as before.
|
|
56
|
+
- bash checks/product-checkout.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
54
57
|
- bash checks/contract-check.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
55
58
|
|
|
56
59
|
# E65/E66 — the risk map (.sdlc/risk-map: a risk level per directory, no names). Warnings, and the
|
|
@@ -77,18 +80,27 @@ yad-lineage-check:
|
|
|
77
80
|
extends: .sdlc_mr_only
|
|
78
81
|
needs: []
|
|
79
82
|
script:
|
|
83
|
+
# E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN CI/CD
|
|
84
|
+
# variable is set (masked); without it this prints a note and the gate defers, as before.
|
|
85
|
+
- bash checks/product-checkout.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
80
86
|
- bash checks/lineage-check.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
81
87
|
|
|
82
88
|
yad-epic-open:
|
|
83
89
|
extends: .sdlc_mr_only
|
|
84
90
|
needs: []
|
|
85
91
|
script:
|
|
92
|
+
# E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN CI/CD
|
|
93
|
+
# variable is set (masked); without it this prints a note and the gate defers, as before.
|
|
94
|
+
- bash checks/product-checkout.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
86
95
|
- bash checks/epic-open.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
87
96
|
|
|
88
97
|
yad-reconcile-debt:
|
|
89
98
|
extends: .sdlc_mr_only
|
|
90
99
|
needs: []
|
|
91
100
|
script:
|
|
101
|
+
# E120: check the Product out where .sdlc/product-link.json says, when the YAD_PRODUCT_TOKEN CI/CD
|
|
102
|
+
# variable is set (masked); without it this prints a note and the gate defers, as before.
|
|
103
|
+
- bash checks/product-checkout.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
92
104
|
- bash checks/reconcile-debt-check.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
93
105
|
|
|
94
106
|
# Pattern gates: commit subject + MR title + MR body all follow the convention (profile: code).
|
|
@@ -2,53 +2,53 @@
|
|
|
2
2
|
# Pattern gates for the PRODUCT, as an INCLUDABLE fragment. Pulled into the Product's root
|
|
3
3
|
# .gitlab-ci.yml via:
|
|
4
4
|
# include:
|
|
5
|
-
# - local: '.gitlab/ci/yad-
|
|
6
|
-
# MRs run with `--profile
|
|
5
|
+
# - local: '.gitlab/ci/yad-product-checks.yml'
|
|
6
|
+
# MRs run with `--profile product` and split by source branch: review/EP-* MRs are Shape artifact-
|
|
7
7
|
# review vehicles (a `review: <artifact> (EP-<slug>)` title + the Product artifact-review template body);
|
|
8
8
|
# every other MR is a tooling/code change to the Product itself and follows the code convention (a
|
|
9
9
|
# Conventional-Commits title + the code task template body). The source branch is passed via --head so
|
|
10
10
|
# the gate can tell the two apart. Commit subjects are always Conventional-Commits.
|
|
11
|
-
# Job names are yad-
|
|
11
|
+
# Job names are yad-product-prefixed so they coexist with any code-repo fragment in one pipeline; jobs use
|
|
12
12
|
# `needs: []` and no `stage:` so a foreign `stages:` list can neither break nor reorder them.
|
|
13
13
|
default:
|
|
14
14
|
image: node:20
|
|
15
15
|
tags: [$YAD_RUNNER_TAGS]
|
|
16
16
|
|
|
17
|
-
.
|
|
17
|
+
.yad_product_mr_only:
|
|
18
18
|
rules:
|
|
19
19
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
20
20
|
|
|
21
21
|
variables:
|
|
22
22
|
GIT_DEPTH: "0" # full history so the commit gate can diff against the target branch
|
|
23
23
|
|
|
24
|
-
yad-
|
|
25
|
-
extends: .
|
|
24
|
+
yad-product-commit-message:
|
|
25
|
+
extends: .yad_product_mr_only
|
|
26
26
|
needs: []
|
|
27
27
|
script:
|
|
28
|
-
- bash checks/commit-message.sh --profile
|
|
28
|
+
- bash checks/commit-message.sh --profile product "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
29
29
|
|
|
30
30
|
# GIT_DEPTH: 0 (above) gives the changed paths, so the gate can reject an artifact change (epics/**)
|
|
31
31
|
# riding a non-review source branch past the Shape review with a plain code title/template.
|
|
32
|
-
yad-
|
|
33
|
-
extends: .
|
|
32
|
+
yad-product-pr-title:
|
|
33
|
+
extends: .yad_product_mr_only
|
|
34
34
|
needs: []
|
|
35
35
|
script:
|
|
36
36
|
- changed="$(mktemp)"; git -c core.quotePath=false diff --no-renames --name-only "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD" > "$changed"
|
|
37
|
-
- bash checks/pr-title.sh --profile
|
|
37
|
+
- bash checks/pr-title.sh --profile product --head "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --changed "$changed" "$CI_MERGE_REQUEST_TITLE"
|
|
38
38
|
|
|
39
|
-
yad-
|
|
40
|
-
extends: .
|
|
39
|
+
yad-product-pr-template:
|
|
40
|
+
extends: .yad_product_mr_only
|
|
41
41
|
needs: []
|
|
42
42
|
script:
|
|
43
43
|
- changed="$(mktemp)"; git -c core.quotePath=false diff --no-renames --name-only "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD" > "$changed"
|
|
44
44
|
- body="$(mktemp)"; printf '%s' "$CI_MERGE_REQUEST_DESCRIPTION" > "$body"
|
|
45
|
-
- bash checks/pr-template.sh --profile
|
|
45
|
+
- bash checks/pr-template.sh --profile product --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/{state,approvals,comments,hub-prs}.json
|
|
47
|
+
# The gate ledger is CI-owned: reject non-bot commits to .sdlc/{state,approvals,comments,product-prs,hub-prs}.json
|
|
48
48
|
# or reviews/*.md (.sdlc/contract-lock.json is artifact-side and allowed). The one other exception is a
|
|
49
49
|
# brand-new epic's seed, which no CI path can write, so it rides this first review MR (#162).
|
|
50
|
-
yad-
|
|
51
|
-
extends: .
|
|
50
|
+
yad-product-ledger-guard:
|
|
51
|
+
extends: .yad_product_mr_only
|
|
52
52
|
needs: []
|
|
53
53
|
script:
|
|
54
54
|
- bash checks/ledger-guard.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
# Needs a CI/CD variable GITLAB_TOKEN (or SDLC_API_TOKEN) with read_api scope — CI_JOB_TOKEN cannot
|
|
10
10
|
# read the commit-signature API. The Product's SDLC_GATE_TOKEN (from the gate-sync wiring) also works:
|
|
11
11
|
# set GITLAB_TOKEN: $SDLC_GATE_TOKEN in the job if you prefer one token.
|
|
12
|
-
# Job name is yad-
|
|
13
|
-
yad-
|
|
12
|
+
# Job name is yad-product-prefixed so it can coexist with the code-repo fragment in one pipeline.
|
|
13
|
+
yad-product-verified-commits:
|
|
14
14
|
needs: []
|
|
15
15
|
# Runner selection: set the YAD_RUNNER_TAGS CI/CD variable (e.g. `dind_runner`) to route this
|
|
16
16
|
# docker job on instances whose runners are tag-locked (run_untagged: false) — it lives in
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// ledger-guard CURSOR ADAPTER — the same guard, answered in the protocol Cursor requires (E11).
|
|
2
|
+
//
|
|
3
|
+
// Cursor's `preToolUse` is a PERMISSION hook, and its docs are explicit: for a permission hook,
|
|
4
|
+
// "invalid JSON or a response that doesn't match the hook's schema blocks the action". Empty stdout is
|
|
5
|
+
// invalid JSON. The plain `ledger-guard.mjs` prints nothing when it allows — so wiring it directly
|
|
6
|
+
// would have blocked EVERY file write in a verified project, which is the exact opposite of this
|
|
7
|
+
// guard's fail-open design.
|
|
8
|
+
//
|
|
9
|
+
// This wrapper guarantees one thing: a permission answer on stdout, always, whatever happens below.
|
|
10
|
+
//
|
|
11
|
+
// allow {"permission":"allow"} exit 0
|
|
12
|
+
// deny {"permission":"deny","user_message":"<reason>","agent_message":"<reason>"} exit 0
|
|
13
|
+
//
|
|
14
|
+
// A deny exits 0 because the JSON is the authoritative answer and exit 0 is what tells Cursor to read
|
|
15
|
+
// it; exit 2 blocks too, but is documented as the code for "no JSON to read" and would discard the
|
|
16
|
+
// reason — and naming the command that owns the transition is the whole point of speaking at edit time.
|
|
17
|
+
//
|
|
18
|
+
// A NODE SCRIPT, NOT A SHELL SCRIPT (E113). On Windows Cursor starts hooks through PowerShell, which
|
|
19
|
+
// cannot run a `.sh` file at all. The entry is `node hooks/ledger-guard-cursor.mjs`: a relative path,
|
|
20
|
+
// because Cursor documents that a project hook runs from the project root.
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const ALLOW = '{"permission":"allow"}';
|
|
27
|
+
const allow = () => { process.stdout.write(`${ALLOW}\n`); process.exit(0); };
|
|
28
|
+
|
|
29
|
+
// ---- yad-resolve (the same block in every hooks/*.mjs; a test keeps the copies identical) ----------
|
|
30
|
+
// The Product root is this script's grandparent — `<product>/hooks/<script>.mjs` — found from the
|
|
31
|
+
// script's OWN location, never from the harness's working directory or its project-root variable
|
|
32
|
+
// (whose form on Windows, `C:\…` or `/c/…`, no harness documents). `realpathSync` so a script reached
|
|
33
|
+
// through a symbolic link still finds its own Product.
|
|
34
|
+
const PRODUCT_ROOT = path.dirname(path.dirname(fs.realpathSync(fileURLToPath(import.meta.url))));
|
|
35
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
36
|
+
|
|
37
|
+
// A command found on PATH, or null. On Windows the npm launchers are `.cmd` files.
|
|
38
|
+
function onPath(name) {
|
|
39
|
+
const exts = IS_WINDOWS ? ['.cmd', '.exe', ''] : [''];
|
|
40
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
|
41
|
+
if (!dir) continue;
|
|
42
|
+
for (const ext of exts) {
|
|
43
|
+
const full = path.join(dir, name + ext);
|
|
44
|
+
try { if (fs.statSync(full).isFile()) return full; } catch { /* not here */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Windows cannot start a `.cmd` launcher without a shell (Node refuses since 18.20.2), so those run
|
|
51
|
+
// through one, each word in double quotes. A Windows path cannot hold a `"`, so the quoting is whole.
|
|
52
|
+
const quoteForCmd = (word) => `"${String(word).replace(/"/g, '')}"`;
|
|
53
|
+
|
|
54
|
+
// How to run `yad`, cheapest and most specific first: an explicit override, then the copy installed in
|
|
55
|
+
// this Product, then `yad` on PATH, then a network-free npx. `--no-install` matters — a hook runs on
|
|
56
|
+
// every tool call and must never pause an agent to download a package. Returns
|
|
57
|
+
// `{ file, args, shell }` or null.
|
|
58
|
+
function resolveYad() {
|
|
59
|
+
// YAD_BIN is commonly an interpreter plus a script ("node /path/to/yad.mjs"). Split on spaces, with a
|
|
60
|
+
// double-quoted word kept whole — Node on Windows usually lives under `C:\Program Files\`.
|
|
61
|
+
const override = [...(process.env.YAD_BIN || '').matchAll(/"([^"]*)"|(\S+)/g)].map((m) => m[1] ?? m[2]).filter(Boolean);
|
|
62
|
+
if (override.length) {
|
|
63
|
+
const [file, ...args] = override;
|
|
64
|
+
return { file, args, shell: IS_WINDOWS && /\.(cmd|bat)$/i.test(file) };
|
|
65
|
+
}
|
|
66
|
+
const local = path.join(PRODUCT_ROOT, 'node_modules', 'yadflow', 'bin', 'yad.mjs');
|
|
67
|
+
if (fs.existsSync(local)) return { file: process.execPath, args: [local], shell: false };
|
|
68
|
+
const yad = onPath('yad');
|
|
69
|
+
if (yad) {
|
|
70
|
+
if (!IS_WINDOWS || !/\.(cmd|bat)$/i.test(yad)) return { file: yad, args: [], shell: false };
|
|
71
|
+
// An npm global install puts the package beside its launcher: run the script with this Node
|
|
72
|
+
// rather than through a shell whenever it is there.
|
|
73
|
+
const script = path.join(path.dirname(yad), 'node_modules', 'yadflow', 'bin', 'yad.mjs');
|
|
74
|
+
if (fs.existsSync(script)) return { file: process.execPath, args: [script], shell: false };
|
|
75
|
+
return { file: yad, args: [], shell: true };
|
|
76
|
+
}
|
|
77
|
+
const npx = onPath('npx');
|
|
78
|
+
if (npx) return { file: npx, args: ['--no-install', 'yadflow'], shell: IS_WINDOWS && /\.(cmd|bat)$/i.test(npx) };
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Run the resolved `yad` with `args` appended. `options` go to spawnSync.
|
|
83
|
+
function runYad(yad, args, options) {
|
|
84
|
+
const all = [...yad.args, ...args];
|
|
85
|
+
if (yad.shell) return spawnSync([yad.file, ...all].map(quoteForCmd).join(' '), { ...options, shell: true, windowsHide: true });
|
|
86
|
+
return spawnSync(yad.file, all, { ...options, windowsHide: true });
|
|
87
|
+
}
|
|
88
|
+
// ---- end yad-resolve ---------------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
// Every failure below this line resolves to ALLOW, because this guard fails open by design and CI is
|
|
91
|
+
// what fails closed. A wrapper that blocked when it could not reach `yad` would stop an agent editing
|
|
92
|
+
// anything at all the moment an install went sideways.
|
|
93
|
+
const yad = resolveYad();
|
|
94
|
+
if (!yad) {
|
|
95
|
+
process.stderr.write(` • yad hook: no \`yad\` on PATH and none installed in ${PRODUCT_ROOT} — allowing (install yadflow to re-arm the ledger guard)\n`);
|
|
96
|
+
allow();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// stdout is CAPTURED (the JSON verdict); stdin (the tool-call payload) and stderr (Cursor's log) are
|
|
100
|
+
// the harness's own.
|
|
101
|
+
const r = runYad(yad, ['hook', 'ledger-guard', '--format', 'cursor'], { stdio: ['inherit', 'pipe', 'inherit'], encoding: 'utf8' });
|
|
102
|
+
|
|
103
|
+
// THE VERDICT IS THE LAST NON-EMPTY LINE, not the whole of stdout, and it is recognised by containing
|
|
104
|
+
// a `"permission"` key rather than by starting with one. `yad`'s own top-level failure handler prints
|
|
105
|
+
// to STDOUT, so a usage error or a crash banner can land in front of the JSON; a prefix test would then
|
|
106
|
+
// fail to recognise our own deny and fall through to the allow below. And a containment test does not
|
|
107
|
+
// care what order the JSON's properties are in.
|
|
108
|
+
const lines = String(r.stdout || '').split(/\r?\n/).filter((l) => l.trim());
|
|
109
|
+
const verdict = lines.length ? lines[lines.length - 1] : '';
|
|
110
|
+
if (verdict.includes('"permission"')) { process.stdout.write(`${verdict}\n`); process.exit(0); }
|
|
111
|
+
|
|
112
|
+
// EXIT 2 IS A DENY, even with nothing on stdout — and this branch is reached in a real, ordinary case.
|
|
113
|
+
// The Product's own `node_modules/yadflow` comes BEFORE `PATH`, so a project pinned to a yadflow older
|
|
114
|
+
// than `--format` parses the flag, ignores it, and answers in the exit protocol: exit 2, empty stdout.
|
|
115
|
+
// The message is FIXED: the reason the older `yad` printed has already gone to stderr, where Cursor
|
|
116
|
+
// logs it.
|
|
117
|
+
if (r.status === 2) {
|
|
118
|
+
process.stdout.write(`${JSON.stringify({
|
|
119
|
+
permission: 'deny',
|
|
120
|
+
user_message: 'yad: this edit touches CI-owned gate state.',
|
|
121
|
+
agent_message: 'This edit touches CI-owned gate state, which CI is the sole writer of. Do not edit the ledger by hand: use `yad gate open` for the transition, and commit only the artifact. The installed yadflow is older than this hook, so the full reason is in the hook log rather than here — `yad doctor` will name the version gap.',
|
|
122
|
+
})}\n`);
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (r.status !== 0) {
|
|
127
|
+
const why = r.error ? r.error.message : `exited ${r.status ?? r.signal}`;
|
|
128
|
+
process.stderr.write(` • yad hook: \`yad hook ledger-guard\` ${why} — allowing (run \`yad doctor\` to check the install)\n`);
|
|
129
|
+
}
|
|
130
|
+
allow();
|