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.
Files changed (128) hide show
  1. package/CHANGELOG.md +699 -0
  2. package/README.md +85 -22
  3. package/bin/commands.mjs +3 -1
  4. package/bin/yad.mjs +183 -29
  5. package/cli/capture.mjs +4 -2
  6. package/cli/checkpoint.mjs +10 -10
  7. package/cli/claims.mjs +4 -4
  8. package/cli/commit.mjs +48 -12
  9. package/cli/docs.mjs +15 -13
  10. package/cli/doctor.mjs +557 -141
  11. package/cli/epic-state.mjs +41 -18
  12. package/cli/errors.mjs +3 -2
  13. package/cli/find-product.mjs +197 -0
  14. package/cli/fold.mjs +4 -4
  15. package/cli/gate-local.mjs +13 -13
  16. package/cli/gate.mjs +111 -103
  17. package/cli/history.mjs +13 -13
  18. package/cli/hook.mjs +201 -30
  19. package/cli/index-command.mjs +6 -6
  20. package/cli/lib.mjs +91 -29
  21. package/cli/manifest.mjs +171 -104
  22. package/cli/migrate.mjs +208 -17
  23. package/cli/mode.mjs +22 -22
  24. package/cli/next.mjs +6 -6
  25. package/cli/openpr.mjs +32 -24
  26. package/cli/people.mjs +22 -10
  27. package/cli/plan.mjs +718 -45
  28. package/cli/platform.mjs +13 -7
  29. package/cli/product-index.mjs +4 -4
  30. package/cli/{hubcommit.mjs → productcommit.mjs} +11 -7
  31. package/cli/protection.mjs +19 -9
  32. package/cli/reconcile.mjs +98 -23
  33. package/cli/repo-publish.mjs +22 -16
  34. package/cli/repo.mjs +128 -33
  35. package/cli/report.mjs +4 -2
  36. package/cli/review.mjs +26 -19
  37. package/cli/setup.mjs +105 -36
  38. package/cli/ship.mjs +1 -1
  39. package/cli/skip.mjs +4 -4
  40. package/cli/tidy.mjs +7 -7
  41. package/cli/update-commit.mjs +22 -19
  42. package/cli/workspace.mjs +495 -0
  43. package/package.json +7 -4
  44. package/skills/sdlc/config.yaml +14 -13
  45. package/skills/sdlc/module-help.csv +8 -8
  46. package/skills/yad-analysis/SKILL.md +6 -4
  47. package/skills/yad-architecture/SKILL.md +4 -3
  48. package/skills/yad-backfill/SKILL.md +11 -5
  49. package/skills/yad-backfill/references/backfill.md +33 -3
  50. package/skills/yad-backfill/templates/checks/backfill-check.sh +200 -17
  51. package/skills/yad-change/SKILL.md +1 -1
  52. package/skills/yad-checks/SKILL.md +48 -27
  53. package/skills/yad-checks/references/check-gates.md +213 -53
  54. package/skills/yad-checks/templates/checks/commit-message.sh +39 -13
  55. package/skills/yad-checks/templates/checks/contract-check.sh +472 -23
  56. package/skills/yad-checks/templates/checks/epic-open.sh +398 -8
  57. package/skills/yad-checks/templates/checks/ledger-guard.sh +75 -25
  58. package/skills/yad-checks/templates/checks/lineage-check.sh +391 -7
  59. package/skills/yad-checks/templates/checks/product-checkout.sh +141 -0
  60. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +399 -8
  61. package/skills/yad-checks/templates/checks/risk-map-check.sh +35 -3
  62. package/skills/yad-checks/templates/checks/spec-link.sh +36 -3
  63. package/skills/yad-checks/templates/checks/verified-commits.sh +26 -2
  64. package/skills/yad-checks/templates/github/yad-checks.yml +33 -13
  65. package/skills/yad-checks/templates/github/{yad-hub-checks.yml → yad-product-checks.yml} +14 -10
  66. package/skills/yad-checks/templates/github/yad-update-guard.yml +1 -1
  67. package/skills/yad-checks/templates/github/yad-verified-commits.yml +1 -1
  68. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +12 -0
  69. package/skills/yad-checks/templates/gitlab/{yad-hub-checks.gitlab-ci.yml → yad-product-checks.gitlab-ci.yml} +16 -16
  70. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +2 -2
  71. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.mjs +130 -0
  72. package/skills/yad-checks/templates/hooks/ledger-guard.mjs +117 -0
  73. package/skills/yad-checks/templates/hooks/yad-capture.mjs +117 -0
  74. package/skills/yad-commit/SKILL.md +18 -7
  75. package/skills/yad-connect-design/SKILL.md +1 -1
  76. package/skills/yad-connect-design/references/design-registry.md +1 -1
  77. package/skills/yad-connect-docs/SKILL.md +16 -11
  78. package/skills/yad-connect-docs/references/docs-registry.md +8 -8
  79. package/skills/yad-connect-learning/SKILL.md +1 -1
  80. package/skills/yad-connect-learning/references/learning-registry.md +1 -1
  81. package/skills/yad-connect-repos/SKILL.md +31 -8
  82. package/skills/yad-connect-repos/references/code-context.md +1 -1
  83. package/skills/yad-connect-repos/references/{hub-config.md → product-config.md} +19 -11
  84. package/skills/yad-connect-repos/references/repos-registry.md +1 -1
  85. package/skills/yad-connect-testing/SKILL.md +1 -1
  86. package/skills/yad-connect-testing/references/testing-registry.md +1 -1
  87. package/skills/yad-discovery/SKILL.md +2 -2
  88. package/skills/yad-discovery/references/discovery-schema.md +1 -1
  89. package/skills/yad-docs/SKILL.md +1 -1
  90. package/skills/yad-docs/references/data-mapping.md +1 -1
  91. package/skills/yad-docs/templates/app/package-lock.json +8 -8
  92. package/skills/yad-docs/templates/app/package.json +1 -1
  93. package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +2 -2
  94. package/skills/yad-docs-overview/references/pipeline-model.md +3 -3
  95. package/skills/yad-engineer-review/SKILL.md +2 -2
  96. package/skills/yad-engineer-review/references/ship-and-record.md +3 -3
  97. package/skills/yad-epic/SKILL.md +6 -4
  98. package/skills/yad-epic/references/state-schema.md +13 -12
  99. package/skills/yad-open-pr/SKILL.md +1 -1
  100. package/skills/yad-pair-review/SKILL.md +5 -5
  101. package/skills/yad-pair-review/references/session-state.md +2 -2
  102. package/skills/yad-pr-template/SKILL.md +14 -12
  103. package/skills/yad-pr-template/templates/checks/pr-template.sh +18 -16
  104. package/skills/yad-pr-template/templates/checks/pr-title.sh +15 -13
  105. package/skills/yad-pr-template/templates/{hub → product}/github/pull_request_template.md +2 -2
  106. package/skills/yad-pr-template/templates/{hub → product}/gitlab/merge_request_templates/Default.md +2 -2
  107. package/skills/{yad-hub-bridge → yad-product-bridge}/SKILL.md +15 -15
  108. package/skills/{yad-hub-bridge → yad-product-bridge}/references/bridge.md +15 -10
  109. package/skills/{yad-hub-bridge → yad-product-bridge}/references/login-roster.md +2 -2
  110. package/skills/{yad-hub-bridge/templates/checks/hub-route.sh → yad-product-bridge/templates/checks/product-route.sh} +2 -2
  111. package/skills/{yad-hub-bridge → yad-product-bridge}/templates/github/yad-gate-sync.yml +29 -16
  112. package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/gitlab-ci.include-root.yml +1 -1
  113. package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/yad-gate-sync.gitlab-ci.yml +5 -3
  114. package/skills/yad-report/SKILL.md +1 -1
  115. package/skills/yad-review-companion/SKILL.md +3 -3
  116. package/skills/yad-review-gate/SKILL.md +22 -23
  117. package/skills/yad-review-gate/references/gating.md +13 -13
  118. package/skills/yad-run/SKILL.md +2 -2
  119. package/skills/yad-run/references/run-loop.md +2 -2
  120. package/skills/yad-spec/SKILL.md +2 -1
  121. package/skills/yad-spec/references/spec-handoff.md +19 -10
  122. package/skills/yad-stories/SKILL.md +2 -2
  123. package/skills/yad-sync-repos/SKILL.md +19 -3
  124. package/skills/yad-test-cases/SKILL.md +2 -2
  125. package/skills/yad-ui/SKILL.md +2 -2
  126. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +0 -91
  127. package/skills/yad-checks/templates/hooks/ledger-guard.sh +0 -100
  128. 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/hubcommit.mjs,
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' < "${SDLC_HUB_CONFIG:-.sdlc/hub.json}" 2>/dev/null | sed -nE 's/.*"default_branch"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')" || _cfg=""
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 hub` and split by head branch:
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-hub-checks
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 hub "origin/${{ github.base_ref }}"
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 hub --head "$PR_HEAD" --changed "$changed" "$PR_TITLE"
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 hub --head "$PR_HEAD" --changed "$changed" "$body"
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-hub-checks.yml'
6
- # MRs run with `--profile hub` and split by source branch: review/EP-* MRs are Shape artifact-
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-hub-prefixed so they coexist with any code-repo fragment in one pipeline; jobs use
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
- .yad_hub_mr_only:
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-hub-commit-message:
25
- extends: .yad_hub_mr_only
24
+ yad-product-commit-message:
25
+ extends: .yad_product_mr_only
26
26
  needs: []
27
27
  script:
28
- - bash checks/commit-message.sh --profile hub "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
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-hub-pr-title:
33
- extends: .yad_hub_mr_only
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 hub --head "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --changed "$changed" "$CI_MERGE_REQUEST_TITLE"
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-hub-pr-template:
40
- extends: .yad_hub_mr_only
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 hub --head "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --changed "$changed" "$body"
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-hub-ledger-guard:
51
- extends: .yad_hub_mr_only
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-hub-prefixed so it can coexist with the code-repo fragment in one pipeline.
13
- yad-hub-verified-commits:
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();