yadflow 3.18.1 → 3.19.0-next.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +11 -11
  3. package/bin/yad.mjs +8 -8
  4. package/cli/artifact-status.mjs +4 -4
  5. package/cli/checkpoint.mjs +25 -25
  6. package/cli/commit.mjs +1 -1
  7. package/cli/companion.mjs +2 -2
  8. package/cli/doctor.mjs +10 -10
  9. package/cli/epic-state.mjs +29 -29
  10. package/cli/errors.mjs +1 -1
  11. package/cli/gate.mjs +32 -33
  12. package/cli/hook.mjs +4 -4
  13. package/cli/hubcommit.mjs +1 -1
  14. package/cli/ledger.mjs +3 -3
  15. package/cli/lib.mjs +23 -9
  16. package/cli/manifest.mjs +42 -21
  17. package/cli/migrate.mjs +54 -12
  18. package/cli/next.mjs +5 -5
  19. package/cli/openpr.mjs +8 -8
  20. package/cli/plan.mjs +28 -9
  21. package/cli/platform.mjs +1 -1
  22. package/cli/report.mjs +1 -1
  23. package/cli/review.mjs +5 -5
  24. package/cli/setup.mjs +22 -10
  25. package/cli/ship.mjs +1 -1
  26. package/cli/skip.mjs +1 -1
  27. package/cli/thread.mjs +1 -1
  28. package/cli/tidy.mjs +2 -2
  29. package/cli/update-commit.mjs +1 -1
  30. package/package.json +1 -1
  31. package/skills/sdlc/config.yaml +30 -30
  32. package/skills/sdlc/module-help.csv +21 -21
  33. package/skills/yad-analysis/SKILL.md +10 -10
  34. package/skills/yad-architecture/SKILL.md +10 -10
  35. package/skills/yad-architecture/references/contract-format.md +2 -3
  36. package/skills/yad-backfill/SKILL.md +5 -5
  37. package/skills/yad-change/SKILL.md +13 -13
  38. package/skills/yad-change/references/triage.md +2 -3
  39. package/skills/yad-checks/SKILL.md +34 -16
  40. package/skills/yad-checks/references/check-gates.md +63 -19
  41. package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
  42. package/skills/yad-checks/templates/checks/epic-open.sh +1 -1
  43. package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
  44. package/skills/yad-checks/templates/checks/ledger-guard.sh +41 -12
  45. package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
  46. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +3 -3
  47. package/skills/yad-checks/templates/github/yad-checks.yml +24 -3
  48. package/skills/yad-checks/templates/github/yad-hub-checks.yml +2 -2
  49. package/skills/yad-checks/templates/github/yad-verified-commits.yml +1 -1
  50. package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
  51. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +12 -3
  52. package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +2 -2
  53. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +1 -1
  54. package/skills/yad-checks/templates/hooks/ledger-guard.sh +1 -1
  55. package/skills/yad-commit/SKILL.md +2 -2
  56. package/skills/yad-connect-design/SKILL.md +1 -1
  57. package/skills/yad-connect-docs/SKILL.md +1 -1
  58. package/skills/yad-connect-repos/SKILL.md +32 -15
  59. package/skills/yad-connect-repos/references/code-context.md +2 -2
  60. package/skills/yad-connect-repos/references/hub-config.md +25 -11
  61. package/skills/yad-connect-repos/references/repos-registry.md +3 -3
  62. package/skills/yad-connect-testing/SKILL.md +1 -1
  63. package/skills/yad-defects/SKILL.md +1 -1
  64. package/skills/yad-discovery/SKILL.md +6 -6
  65. package/skills/yad-discovery/references/discovery-schema.md +1 -1
  66. package/skills/yad-docs/SKILL.md +3 -3
  67. package/skills/yad-docs-overview/SKILL.md +3 -3
  68. package/skills/yad-docs-overview/references/pipeline-model.md +17 -11
  69. package/skills/yad-engineer-review/SKILL.md +9 -9
  70. package/skills/yad-engineer-review/references/ship-and-record.md +8 -8
  71. package/skills/yad-epic/SKILL.md +15 -15
  72. package/skills/yad-epic/references/state-schema.md +30 -30
  73. package/skills/yad-hub-bridge/SKILL.md +14 -14
  74. package/skills/yad-hub-bridge/references/bridge.md +17 -17
  75. package/skills/yad-hub-bridge/references/login-roster.md +3 -3
  76. package/skills/yad-hub-bridge/templates/checks/hub-route.sh +1 -1
  77. package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +1 -1
  78. package/skills/yad-implement/SKILL.md +3 -3
  79. package/skills/yad-open-pr/SKILL.md +4 -4
  80. package/skills/yad-pair-review/SKILL.md +12 -12
  81. package/skills/yad-pair-review/references/session-state.md +3 -3
  82. package/skills/yad-pr-template/SKILL.md +4 -4
  83. package/skills/yad-pr-template/references/risk-routing.md +1 -1
  84. package/skills/yad-pr-template/templates/checks/pr-template.sh +18 -10
  85. package/skills/yad-pr-template/templates/checks/pr-title.sh +7 -7
  86. package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +1 -1
  87. package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +1 -1
  88. package/skills/yad-reconcile/SKILL.md +1 -1
  89. package/skills/yad-report/SKILL.md +1 -1
  90. package/skills/yad-review-companion/SKILL.md +7 -7
  91. package/skills/yad-review-gate/SKILL.md +18 -18
  92. package/skills/yad-review-gate/references/gating.md +3 -3
  93. package/skills/yad-run/SKILL.md +10 -10
  94. package/skills/yad-run/references/run-loop.md +8 -8
  95. package/skills/yad-ship/SKILL.md +4 -4
  96. package/skills/yad-spec/SKILL.md +10 -11
  97. package/skills/yad-status/SKILL.md +13 -13
  98. package/skills/yad-stories/SKILL.md +12 -12
  99. package/skills/yad-stories/references/story-schema.md +3 -3
  100. package/skills/yad-stub/SKILL.md +3 -3
  101. package/skills/yad-sync-repos/SKILL.md +1 -1
  102. package/skills/yad-test-cases/SKILL.md +12 -13
  103. package/skills/yad-test-cases/references/test-cases-schema.md +1 -1
  104. package/skills/yad-ui/SKILL.md +10 -10
@@ -7,21 +7,21 @@ description: 'Phase 6 post-lock change management — the change-request/defect
7
7
 
8
8
  **Goal:** Turn a post-lock change request, defect, or hotfix into a **new epic threaded to its parent**,
9
9
  so the feature's locked artifacts are never mutated — only *superseded*. The change-epic **inherits**
10
- the front artifacts it does not change (by reference) and **re-authors** only the ones it does, so the
10
+ the Shape artifacts it does not change (by reference) and **re-authors** only the ones it does, so the
11
11
  thread head always describes current behaviour and the SDLC stays a trusted source of truth for the next
12
12
  change. This skill does the **intake + triage + seeding** and then hands off to the normal authoring
13
- skills + `yad-review-gate`. It is a **front state**: human-confirmed, **never auto-advances**.
13
+ skills + `yad-review-gate`. It is a **Shape step**: human-confirmed, **never auto-advances**.
14
14
 
15
- This is the answer to "the front/spec docs go stale after the contract locks": a behavioural change can
16
- no longer ship through the build half against an old story — `epic-open` seals a fully-shipped epic, so
15
+ This is the answer to "the Shape/spec docs go stale after the contract locks": a behavioural change can
16
+ no longer ship through Build against an old story — `epic-open` seals a fully-shipped epic, so
17
17
  new behaviour must enter here, and its re-authored stories/test-cases describe the change.
18
18
 
19
19
  ## Conventions
20
20
 
21
21
  - `{project-root}` resolves from the product hub.
22
22
  - Artifacts live under `{project-root}/epics/EP-<slug>/` — the change-epic gets its OWN `EP-<slug>`
23
- (assigned here, never renamed) and its own `stories/EP-<slug>-S0N`, so every existing gate, the bridge,
24
- `yad next`, and the build-half traceability keep working unchanged.
23
+ (assigned here, never renamed) and its own `stories/EP-<slug>-S0N`, so every existing gate, the verified ledger,
24
+ `yad next`, and the Build traceability keep working unchanged.
25
25
  - The thread is **derived** from `parent:` frontmatter (no registry); `thread:` is a cache that must
26
26
  equal the computed root (`yad doctor` flags a mismatch). Thread id = the genesis epic's id.
27
27
  - Lineage frontmatter, the inherited-step shape, the pointer-lock, `change.json`, and
@@ -67,7 +67,7 @@ in `references/triage.md`).
67
67
 
68
68
  ### Step 2 — Gather the change + triage the DEPTH (auto-propose, human-confirm)
69
69
  With the requester, classify the change into one **depth** (the `yad-backfill` discipline: auto-propose,
70
- human-confirm). The depth decides which front states are **re-authored** vs **inherited**:
70
+ human-confirm). The depth decides which Shape steps are **re-authored** vs **inherited**:
71
71
 
72
72
  | depth | re-authors (active) | inherits (pre-done, by reference) | first step |
73
73
  |-------|---------------------|-----------------------------------|-----------|
@@ -121,7 +121,7 @@ by `yad-architecture` downstream.
121
121
 
122
122
  ### Step 5 — Seed `state.json` (inherited steps pre-done; only the changed steps run)
123
123
  Create `.sdlc/state.json` with the **same 10-step chain** as `yad-epic` (so `advanceState`/`nextAction`/
124
- `gatePredicate`/the bridge run unchanged), but:
124
+ `gatePredicate`/the verified ledger run unchanged), but:
125
125
  - **Inherited** authoring steps **and their review gates**: `status: "done"`, `"inherited": true`,
126
126
  `"inheritedFrom": "<owning epic from the resolved truth>"`, `"boundHash": "<that artifact's current
127
127
  hash>"`.
@@ -136,7 +136,7 @@ Seed `.sdlc/comments.json` = `[]` and create `reviews/`.
136
136
 
137
137
  Commit the seed on the `change/EP-<slug>` branch. It reaches the hub's default branch through this
138
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,
139
+ it carries the seed. In verified mode `ledger-guard` exempts a new epic's ledger (creation, not mutation,
140
140
  #162), so no direct push to a protected default branch is needed; every later change to that ledger is
141
141
  CI's. See `../yad-epic/references/state-schema.md`, "Authoring branches".
142
142
 
@@ -166,16 +166,16 @@ defect to the gate that should have caught it. Add `"parentStub": true` when the
166
166
  un-promoted stub (Step 5) — omit it (or `false`) otherwise.
167
167
 
168
168
  ### Step 7 — Hotfix only: record the ship-first exception + open reconcile debt
169
- If `kind: hotfix`, the build half MAY run before these front gates approve (severity demands it). Record
169
+ If `kind: hotfix`, Build MAY run before these Shape gates approve (severity demands it). Record
170
170
  `hotfix: { "shipFirst": true }` in `change.json` and **append** to `.sdlc/reconcile-debt.json`:
171
171
  `{ "thread": "<…>", "epicId": "<…>", "openedDate": "<today>", "reason": "<why>", "requires": ["artifacts-updated","regression-test"], "status": "open", "paidDate": null, "paidBy": null, "evidence": { "artifacts": [], "regressionTest": "" } }`.
172
172
  Tell the user the debt **freezes the next normal change** on this thread (`reconcile-debt` gate) until it
173
- is paid (front artifacts updated **and** a regression test added, then `status: "paid"`).
173
+ is paid (Shape artifacts updated **and** a regression test added, then `status: "paid"`).
174
174
 
175
175
  ### Step 8 — Stop; hand off (NO auto-advance)
176
176
  Report: the new `EP-<slug>`, its thread + parent, the re-author-vs-inherit split, the seeded
177
177
  `currentStep`, and the next skill — `yad-architecture` (contract-surface), else `yad-stories` /
178
- `yad-test-cases` — followed by `yad-review-gate`. Front states do not auto-advance. Suggest
178
+ `yad-test-cases` — followed by `yad-review-gate`. Shape steps do not auto-advance. Suggest
179
179
  `yad thread <thread>` to see the evolution and `yad-timeline` / `yad-defects` to render it.
180
180
 
181
181
  ## Hard rules
@@ -186,7 +186,7 @@ Report: the new `EP-<slug>`, its thread + parent, the re-author-vs-inherit split
186
186
  `boundHash`; the pointer-lock carries the parent hash verbatim. The gate never re-reviews them.
187
187
  - **Contract-surface ⇒ re-author architecture.** Omitting `architecture` from `inherits` is the ONLY way
188
188
  to change the surface; it re-locks (new hash) and routes through the escalated architecture review —
189
- the same mechanism as the build-half `Contract-Change` route, unified.
189
+ the same mechanism as the Build `Contract-Change` route, unified.
190
190
  - **A hotfix opens debt, never waives it.** Ship-first is allowed once; the thread freezes for new work
191
191
  until the debt is paid.
192
192
  - **Never auto-advances.** This skill seeds + records; humans author and approve via the normal gates.
@@ -69,15 +69,14 @@ Write `.sdlc/contract-lock.json` with the parent's hash **verbatim**:
69
69
  ```
70
70
 
71
71
  Get the parent hash from the owning epic's `.sdlc/contract-lock.json` `hash` field (do NOT recompute — copy
72
- it). `contract-check.sh` reads only `hash`, so a build-half story in the change-epic pins this identical
72
+ it). `contract-check.sh` reads only `hash`, so a Build story in the change-epic pins this identical
73
73
  hash via its `link.md` and the gate passes unchanged. There is no `contract.md` in the change-epic, so
74
74
  the surface physically cannot drift.
75
75
 
76
76
  **To CHANGE the surface instead:** do not inherit `architecture`. Then `yad-architecture` re-authors
77
77
  `contract.md` in the change-epic between fresh `CONTRACT-SURFACE` markers, computes a **new** hash, and
78
78
  writes a real (non-pointer) `contract-lock.json`. `architecture-review` carries `risk_tags: ["contract"]`
79
- → the escalated domain-owner review. This is the same re-lock-invalidates-approvals behaviour the front
80
- half already has, relocated from "edit the locked file" to "author a contract-surface change-epic".
79
+ → the escalated domain-owner review. This is the same re-lock-invalidates-approvals behaviour Shape already has, relocated from "edit the locked file" to "author a contract-surface change-epic".
81
80
 
82
81
  ## Genesis migration (one-time, per feature)
83
82
 
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: yad-checks
3
- description: 'Build-half Step C of the gated SDLC — the production-safety check gates. Wire and run the CI gates on a code repo: spec-link (every change links a real story/spec via its Task trailer), contract-check (a diff that changes the contract surface without a Contract-Change + an updated, re-locked contract FAILS and routes back to the architecture gate), build/test/lint, verified-commits (no unverified commits from unverified users — platform-Verified signature + roster-allowlisted author, on the hub and every repo), and the Phase 6 feature-thread gates lineage-check / epic-open / reconcile-debt (a change links a real threaded epic; a sealed epic refuses new behaviour; a thread with open hotfix debt is frozen until paid). The gates are CI-agnostic bash, invoked by GitHub Actions and GitLab CI. Use when the user says "wire the check gates", "run the gates", "require signed commits", or "set up CI checks" for a repo.'
3
+ description: 'Build Step C of the gated SDLC — the production-safety check gates. Wire and run the CI gates on a code repo: spec-link (every change links a real story/spec via its Task trailer), contract-check (a diff that changes the contract surface without a Contract-Change + an updated, re-locked contract FAILS and routes back to the architecture gate), build/test/lint, verified-commits (no unverified commits from unverified users — platform-Verified signature + roster-allowlisted author, on the hub and every repo), and the Phase 6 feature-thread gates lineage-check / epic-open / reconcile-debt (a change links a real threaded epic; a sealed epic refuses new behaviour; a thread with open hotfix debt is frozen until paid). The gates are CI-agnostic bash, invoked by GitHub Actions and GitLab CI. Use when the user says "wire the check gates", "run the gates", "require signed commits", or "set up CI checks" for a repo.'
4
4
  ---
5
5
 
6
- # SDLC — Check Gates (build-half Step C)
6
+ # SDLC — Check Gates (Build Step C)
7
7
 
8
8
  **Goal:** Install and run the **check gates** that protect production for a code repo. They run
9
9
  in CI on every PR/MR and must pass before merge (build plan §C). Each is a small, separate check:
@@ -17,6 +17,16 @@ in CI on every PR/MR and must pass before merge (build plan §C). Each is a smal
17
17
  never widened from inside a code repo (Phase 2 contract representation: delimited block + SHA-256 lock).
18
18
  Every story whose slice the diff touches is checked, not just the first — see `references/check-gates.md`.
19
19
  3. **build/test/lint** — standard quality stage; tests must actually exercise new behavior, not just pass.
20
+ CI installs and runs through the package manager declared by the repo's standard
21
+ `package.json#packageManager` field (`npm` and `pnpm` are supported; any other manager stays on
22
+ npm with a warning when an npm lockfile is present, and fails closed otherwise); an npm repo
23
+ without that field retains the historical npm behavior. A declared manager requires a full, exact semantic
24
+ version (with Corepack's optional integrity suffix supported), and CI activates that exact version.
25
+ The code-repo `yad-checks` workflow uses Node 22 by default (the build/test/lint job on GitHub; every
26
+ `yad-*` gate job's image in the GitLab fragment) and reads `YAD_NODE_VERSION` (a GitHub repository
27
+ variable or GitLab CI/CD variable) for repos whose declared runtime differs (a declared `packageManager` needs Corepack, so the override must be a line that
28
+ bundles a current one: 20.19+, 22.14+ or 24; Node 25+ dropped it). The hub-side workflows (verified-commits, hub-checks, update-guard,
29
+ gate-sync) only run the `yad` CLI and keep their own pinned Node; the variable does not reach them.
20
30
  The CI job sets `YAD_TEST_MAX_WORKERS` (default `2`); the gate caps jest/vitest test concurrency at
21
31
  that and is a no-op for other runners (see `references/check-gates.md`).
22
32
  4. **verified-commits** — no unverified commits from unverified users: every commit in the range must
@@ -30,7 +40,7 @@ in CI on every PR/MR and must pass before merge (build plan §C). Each is a smal
30
40
  resolution; the every-code-change-has-a-threaded-epic enforcement.
31
41
  6. **epic-open** (Phase 6, the staleness preventer) — an epic is **sealed** once every story is
32
42
  `shipped`; a commit targeting a sealed epic **FAILS**, forcing new behaviour into a new threaded
33
- change-epic (so the front artifacts can never go stale).
43
+ change-epic (so the Shape artifacts can never go stale).
34
44
  7. **reconcile-debt** (Phase 6) — a hotfix that shipped first opens debt; the **next** change on its
35
45
  thread **FAILS** until the debt is paid (artifacts updated + a regression test added).
36
46
 
@@ -50,10 +60,11 @@ and GitLab CI. This step is **by hand** in Phase 3 — run the gates with the sk
50
60
  (`config.yaml` `build.code_repos_root`).
51
61
  - Canonical gate sources live in this skill's `templates/` (the source of truth that gets installed
52
62
  into each code repo):
53
- - `templates/checks/{spec-link,contract-check,build-test-lint,verified-commits}.sh`
54
- - `templates/checks/ledger-guard.sh` → **hub-only** gate, active **only in bridge mode** — hub.json
55
- carries BOTH a `platform` and `bridge_enabled` (or the legacy `bridge`) true, the same predicate
56
- `isBridge` (`cli/gate.mjs`) applies, so the gate and the CLI can never disagree about who owns the
63
+ - `templates/checks/{spec-link,contract-check,package-manager,install-deps,build-test-lint,verified-commits}.sh`
64
+ - `templates/checks/ledger-guard.sh` → **hub-only** gate, active **only in verified mode** — hub.json
65
+ carries BOTH a `platform` and `ledger: "verified"` — or, before `yad migrate`, `bridge_enabled`
66
+ (or the legacy `bridge`) true. The same predicate
67
+ `isVerifiedLedger` (`cli/manifest.mjs`) applies, so the gate and the CLI can never disagree about who owns the
57
68
  ledger (#186). A no-op otherwise, when humans legitimately own it. On review PRs it FAILs any
58
69
  commit that touches the
59
70
  CI-owned gate ledger (`.sdlc/{state,approvals,comments,hub-prs}.json`, `reviews/*.md`) unless it
@@ -65,17 +76,24 @@ and GitLab CI. This step is **by hand** in Phase 3 — run the gates with the sk
65
76
  default branch the guard is absolute again. Runs in `yad-hub-checks`
66
77
  alongside `verified-commits` (which waives the allowlist for the bot but still requires its
67
78
  signature). See `yad-hub-bridge`.
68
- - `templates/hooks/ledger-guard.sh` → **hub-only** agent guardrail, active **only in bridge mode**
69
- (the same `isBridgeHub` predicate). Not a CI gate: it is a **harness hook** that refuses an agent's
79
+ - `templates/hooks/ledger-guard.sh` → **hub-only** agent guardrail, active **only in verified mode**
80
+ (the same `isVerifiedLedger` predicate). Not a CI gate: it is a **harness hook** that refuses an agent's
70
81
  edit to the CI-owned ledger at the moment it is attempted and names `yad gate open` instead — the
71
82
  local half of `checks/ledger-guard.sh` (#171). Installed to `<hub>/hooks/ledger-guard.sh` with the
72
83
  `PreToolUse` entry in `.claude/settings.json`. Fails OPEN; see "Step 2b" below.
73
84
  - `templates/github/yad-verified-commits.yml` + `templates/gitlab/yad-verified-commits.gitlab-ci.yml`
74
85
  → the standalone hub-side verified-commits CI (installed by `yad check --fix` with the hub wiring)
75
- - `templates/github/yad-checks.yml` → installs to `.github/workflows/yad-checks.yml` (marked `# yad-managed: yad-checks`)
76
- - `templates/gitlab/yad-checks.gitlab-ci.yml` → includable fragment, installs to `.gitlab/ci/yad-checks.yml`
86
+ - `templates/github/yad-checks.yml` → installs to `.github/workflows/yad-checks.yml` (marked `# yad-managed: yad-checks`);
87
+ its quality job uses `install-deps.sh`, reads the optional `YAD_NODE_VERSION` repository variable,
88
+ exports the PR's exact `NX_BASE`/`NX_HEAD`, and filters unrelated history blobs while preserving
89
+ the full commit graph.
90
+ - `templates/gitlab/yad-checks.gitlab-ci.yml` → includable fragment, installs to `.gitlab/ci/yad-checks.yml`;
91
+ it uses the same installer, honors a project/group `YAD_NODE_VERSION` CI/CD variable, and exports
92
+ the MR's exact `NX_BASE`/`NX_HEAD`.
77
93
  - `templates/gitlab/gitlab-ci.include-root.yml` → minimal root written only when no root `.gitlab-ci.yml` exists
78
- - `templates/gitlab/.gitlab-ci.yml` → legacy standalone root (greenfield single-file option)
94
+ - `templates/gitlab/.gitlab-ci.yml` → legacy standalone root (greenfield single-file option), kept
95
+ semantically aligned with the includable fragment's Node version, Nx range, dependency install,
96
+ and test-worker cap
79
97
  - The gates depend on the conventions from earlier steps: the `Task:`/`Contract-Change:` commit
80
98
  trailers (`yad-implement`), the `specs/<story>/link.md` + `contracts/` slice (`yad-spec`), and the
81
99
  locked `contract.md` (`yad-architecture`).
@@ -151,9 +169,9 @@ with a hub-flavored gate set — see "Wiring the hub" in `references/check-gates
151
169
  `<hub>/hooks/ledger-guard.sh`, plus the `PreToolUse` entry in `.claude/settings.json`. `yad setup`
152
170
  and `yad check --fix` install both; there is nothing to do by hand.
153
171
 
154
- ### Step 2b — the agent guardrail (harness hooks, bridge mode only)
172
+ ### Step 2b — the agent guardrail (harness hooks, verified mode only)
155
173
  The CI gates speak at CI time. That is too late for one failure the field kept hitting (#171): in
156
- bridge mode the gate ledger is **CI-owned**, so an agent that hand-edits
174
+ verified mode the gate ledger is **CI-owned**, so an agent that hand-edits
157
175
  `epics/*/.sdlc/state.json` only finds out twenty minutes later, from a `ledger-guard` FAIL with
158
176
  nothing connecting cause to effect — and by then the write has to be reverted before the review
159
177
  PR/MR can go green.
@@ -171,7 +189,7 @@ file-editing tool call and refuses the write up front, naming the command that o
171
189
  `contract-lock.json`, `change.json`, and every artifact. A **new** epic's ledger is exempt too —
172
190
  creation, not mutation (#162), decided by listing the epics the **base ref** carries (an
173
191
  `origin/` ref, case-folded slugs), never by looking at the working tree.
174
- - **A no-op without the bridge.** There the ledger is locally owned and the hand-edit the authoring
192
+ - **A no-op with a local ledger.** There the ledger is locally owned and the hand-edit the authoring
175
193
  skills describe is *correct*, so nothing is wired and nothing is blocked.
176
194
  - **It fails OPEN** — no `yad`, no hub, an unreadable config, an unparseable payload all ALLOW, with
177
195
  a note on stderr. `ledger-guard` in CI fails *closed* and remains the authority. `YAD_HOOK_DISABLE=1`
@@ -181,7 +199,7 @@ file-editing tool call and refuses the write up front, naming the command that o
181
199
  hub**, since a harness reads hooks from its own project root — a session opened at the workspace
182
200
  above the hub never loads the hub's `.claude/settings.json`.
183
201
 
184
- `yad doctor` reports the guardrail as `agent ledger guard wired` / `not wired` on a bridge hub.
202
+ `yad doctor` reports the guardrail as `agent ledger guard wired` / `not wired` on a verified hub.
185
203
  See `references/check-gates.md` §"The agent guardrail".
186
204
 
187
205
  ### Step 3 — `run` (run the gates now)
@@ -1,6 +1,6 @@
1
1
  # Check gates — definitions, scripts, CI wiring, convention map
2
2
 
3
- The gates are the production-safety core of the build half (Phase 3 build plan §C). They are
3
+ The gates are the production-safety core of Build (Phase 3 build plan §C). They are
4
4
  deliberately small, separate, and CI-agnostic: plain bash in `checks/`, invoked by whatever CI the
5
5
  repo uses. Each reads conventions established by earlier steps — it invents nothing.
6
6
 
@@ -10,7 +10,7 @@ repo uses. Each reads conventions established by earlier steps — it invents no
10
10
  |------|-------|-------------|
11
11
  | spec-link | the `Task: <story>-<task>` commit trailer; `specs/<story>/link.md` | `yad-implement` (trailer), `yad-spec` (link.md) |
12
12
  | contract-check | changed files under `specs/<story>/contracts/`; the `Contract-Change: yes` trailer; `link.md`'s pinned `contract-lock`; the product repo's `contract-lock.json` | `yad-architecture` (lock), `yad-spec` (slice + link), `yad-implement` (trailer) |
13
- | build/test/lint | the repo's `npm run lint` / `npm run build` / `npm test` | the repo |
13
+ | build/test/lint | the repo's configured package manager running `lint` / `build` / `test` | the repo |
14
14
  | lineage-check | the `Task:` trailer → `link.md` (`epic` + `product-repo`); the owning epic's `kind`/`parent` frontmatter in the hub | `yad-spec` (link.md), `yad-change` (lineage frontmatter) |
15
15
  | epic-open | the `Task:` trailer → `link.md` → the hub epic's `stories/*.md` `status:` (sealed = all `shipped`) | `yad-engineer-review` (story status), `yad-change` (the change-epic) |
16
16
  | reconcile-debt | the `Task:` trailer → `link.md` → the hub epic's `thread`; every thread epic's `reconcile-debt.json` | `yad-change` (opens hotfix debt) |
@@ -93,13 +93,44 @@ own CI runs, plus an assertion that each one actually *assigns* `BASE` from it.
93
93
 
94
94
  ## 3. build/test/lint (`templates/checks/build-test-lint.sh`)
95
95
 
96
- - Runs `npm run lint`, `npm run build`, `npm test` in order; any non-zero exit fails the gate.
96
+ - Reads the standard `package.json#packageManager` field and runs `lint`, `build`, and `test` through
97
+ that manager in order; any non-zero exit fails the gate. `npm` and `pnpm` are supported; another
98
+ manager (yarn, bun) fails closed unless an npm lockfile is present, in which case the repo stays on
99
+ the historical npm path with a warning — a yarn-locally, npm-in-CI repo was green before this
100
+ field was read and must not go red on upgrade. A repo
101
+ without the field retains npm behavior unless it carries `pnpm-lock.yaml` and no npm lockfile
102
+ (`package-lock.json` / `npm-shrinkwrap.json`) — a repo with both keeps the npm path rather than
103
+ silently changing toolchains.
104
+ - CI runs `install-deps.sh` first. A declared npm or pnpm manager must use a full, exact semantic
105
+ version with either no build metadata or Corepack's integrity suffix,
106
+ `+<sha1|sha224|sha256|sha384|sha512>.<lowercase hex digest>` at that algorithm's digest length
107
+ (Corepack hashes the download with the named algorithm and compares the lowercase hex string).
108
+ Other build metadata, digest algorithms, digest lengths, uppercase digests, or trailing
109
+ identifiers fail closed. The installer activates the declared version through
110
+ Corepack. Pinned npm is dispatched through Corepack — `corepack npm ci` here and `corepack npm run`
111
+ in the gate, since Corepack activates npm but never shims it — so Node's ambient npm cannot
112
+ override the declaration anywhere; packageManager-absent npm preserves the historical `npm ci` path. pnpm uses
113
+ `pnpm install --frozen-lockfile`. Partial versions, ranges, and tags also fail closed instead of
114
+ floating to a different toolchain.
115
+ - The build/test/lint job defaults to Node 22. Set the GitHub repository variable or GitLab
116
+ project/group CI/CD variable `YAD_NODE_VERSION` when the repo requires another supported Node
117
+ release; generated files remain managed instead of accumulating consumer-specific edits. The
118
+ variable is read by the code repo's `yad-checks` workflow only (on GitHub the build/test/lint job;
119
+ in the GitLab fragment it sets the `node:` image of every `yad-*` gate job, which all share one
120
+ anchor) — the hub-side workflows run the `yad` CLI, not the repo's toolchain, and keep their own
121
+ pinned Node. A declared `packageManager` needs
122
+ Corepack, which ships with Node 18 through 24 (Node 25+ dropped it) and, before 18.20.7 / 20.19 /
123
+ 22.14, carries registry keys too old to verify anything published since 2025 — so the override
124
+ should stay on a current 20, 22 or 24 line (or install a current Corepack first). The installer
125
+ checks for the binary and wraps `corepack prepare`, so both an absent and a stale Corepack fail
126
+ with that guidance instead of a bare "command not found" or "Cannot find matching keyid".
97
127
  - Tests must actually exercise behavior (build plan §C) — an empty or trivially-passing suite does not
98
128
  satisfy the gate's intent.
99
129
  - **Test worker cap.** When the CI job sets `YAD_TEST_MAX_WORKERS` (the templates default it to `2`)
100
130
  and the repo's `test` script is jest/vitest, the gate forwards `--maxWorkers=<n>` to bound CI
101
- concurrency. For any other runner (`node --test`, mocha, …) it is a no-op — the flag is never
102
- passed, so the gate cannot break on an unknown option. Override it per repo via the
131
+ concurrency (as `-- --maxWorkers=<n>` under npm, which consumes the separator, and bare under pnpm,
132
+ which would forward a literal `--` to the script). For any other runner (`node --test`, mocha, …)
133
+ it is a no-op — the flag is never passed, so the gate cannot break on an unknown option. Override it per repo via the
103
134
  `YAD_TEST_MAX_WORKERS` CI variable, or unset it to remove the cap.
104
135
 
105
136
  ### Canonical `package.json` scripts (Node demo)
@@ -176,13 +207,13 @@ from the event payload):
176
207
  period (`config.yaml build.pr_title_style: same_as_commit_subject` — one task = one PR, the title is
177
208
  the squash-merge subject).
178
209
  - `--profile hub` → splits by the PR/MR **head branch** (passed via `--head`, injected by CI):
179
- - `review/EP-*` head (or no `--head` — stays strict) → a front-half artifact-review title
210
+ - `review/EP-*` head (or no `--head` — stays strict) → a Shape artifact-review title
180
211
  `review: <artifact> (EP-<slug>)`, the shape `yad gate open` creates.
181
212
  - any other head → a tooling/code change to the hub itself, so it follows the `code` convention (a
182
213
  Conventional-Commits subject). This is what lets a PR that changes the hub's own workflows/checks
183
214
  pass — it has no EP artifact to review.
184
215
  - **Anti-bypass guard.** The branch name alone is not trusted: a non-review head that actually
185
- changes front-half artifacts (any path under `epics/**`) **FAILS** — those changes must go through
216
+ changes Shape artifacts (any path under `epics/**`) **FAILS** — those changes must go through
186
217
  a `review/EP-*` PR and the artifact-review workflow. CI passes the PR's changed paths via
187
218
  `--changed <file>` (computed from the diff against the base ref); without that list (a direct
188
219
  by-hand caller) the guard is inert and the branch split alone applies.
@@ -196,10 +227,10 @@ catches a free-form description that bypassed it:
196
227
  `Risk level:` (`low|medium|high`).
197
228
  - `--profile hub` → splits by the PR/MR **head branch** (passed via `--head`, injected by CI):
198
229
  - `review/EP-*` head (or no `--head`) → requires the artifact-review template: `## Artifact under
199
- review`, `## Impact & Risk (front-half)`, `## Checklist`, and a `Risk tags:` line.
230
+ review`, `## Impact & Risk (front-half)` (or `(Shape)`), `## Checklist`, and a `Risk tags:` line.
200
231
  - any other head → a hub tooling PR, so it requires the `code` task template (`## Summary`,
201
232
  `## Impact & Risk`, `## Checklist`, filled `Risk level:`).
202
- - **Anti-bypass guard** (same as pr-title): a non-review head that changes front-half artifacts
233
+ - **Anti-bypass guard** (same as pr-title): a non-review head that changes Shape artifacts
203
234
  (`epics/**`, detected from the CI-supplied `--changed <file>` list) **FAILS** — artifact changes
204
235
  must go through a `review/EP-*` PR.
205
236
 
@@ -244,7 +275,7 @@ four copies stay byte-identical.
244
275
  epic in a thread" enforcement, layered on spec-link.
245
276
  - **epic-open** — an epic is **sealed** iff it has ≥1 story and **every** `stories/*.md` `status:` is
246
277
  `shipped`. A commit whose owning epic is sealed **FAILS**: new behaviour cannot mutate a shipped epic;
247
- it must land in a new threaded change-epic. This is what stops the front artifacts from going stale.
278
+ it must land in a new threaded change-epic. This is what stops the Shape artifacts from going stale.
248
279
  - **reconcile-debt** — resolves the epic's `thread` (its `thread:` frontmatter, else the epic id) and
249
280
  scans every thread epic's `reconcile-debt.json`. An **open** entry the current epic does not own
250
281
  **FAILS** the change (the thread is frozen until the hotfix debt is paid: artifacts updated + a
@@ -296,11 +327,24 @@ The gates run identically under either CI; the config just invokes the scripts w
296
327
  read the title/body from the event payload: `pr-title` takes `${{ github.event.pull_request.title }}`
297
328
  and `pr-template` writes `${{ github.event.pull_request.body }}` to a temp file. All `--profile code`.
298
329
  The Phase 6 thread gates (`lineage-check`, `epic-open`, `reconcile-debt`) run as their own jobs with
299
- `fetch-depth: 0`, the same `origin/${{ github.base_ref }}` base.
330
+ `fetch-depth: 0`, the same `origin/${{ github.base_ref }}` base. The build/test/lint checkout also
331
+ uses `filter: blob:none`; its installer follows `package.json#packageManager`, and `NX_BASE` /
332
+ `NX_HEAD` carry the exact base/head SHAs so Nx affected commands evaluate the PR rather than a
333
+ stale default. `YAD_NODE_VERSION` is read from GitHub repository variables with `22` as the default.
334
+ Dependencies are cached with `actions/cache` (npm's `~/.npm`, pnpm's store, and the Corepack home
335
+ holding the pinned manager, keyed on the lockfiles and package.json) rather than setup-node's
336
+ npm-only `cache:`, which must name the manager before package.json has been read.
300
337
  - **GitLab CI** — `templates/gitlab/yad-checks.gitlab-ci.yml` → `.gitlab/ci/yad-checks.yml`, pulled in
301
338
  by the root `.gitlab-ci.yml`'s `include:`. The jobs run on `merge_request_event` with `GIT_DEPTH: 0`,
302
339
  passing `origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`; the pattern jobs read `$CI_MERGE_REQUEST_TITLE`
303
- and `$CI_MERGE_REQUEST_DESCRIPTION`. All `--profile code`.
340
+ and `$CI_MERGE_REQUEST_DESCRIPTION`. The quality job uses the same package-manager-aware installer;
341
+ `NX_BASE=$CI_MERGE_REQUEST_DIFF_BASE_SHA` and `NX_HEAD=$CI_COMMIT_SHA` provide the equivalent Nx
342
+ range. Its `node:${YAD_NODE_VERSION}` image defaults to `22` and a project/group CI/CD variable may
343
+ override it without editing the managed fragment. Those three variables sit on the `.sdlc_mr_only`
344
+ anchor, not the fragment's top-level `variables:` — an included top-level block merges into the host
345
+ pipeline's globals, where `NX_BASE`/`NX_HEAD` could collide with a host's own Nx setup. The retained greenfield standalone template
346
+ (`templates/gitlab/.gitlab-ci.yml`) carries the same Node default, exact Nx range, and dependency
347
+ installer and test-worker-cap semantics. All `--profile code`.
304
348
 
305
349
  ## Sync with existing CI (merge, never clobber)
306
350
 
@@ -344,7 +388,7 @@ merge-not-clobber logic, with a **hub-flavored gate set** appropriate to a "thin
344
388
  - **approvals-present** — an epic at `ready-for-build` has the approvals its gate rule requires recorded
345
389
  in `.sdlc/approvals.json` (the same predicate `yad-review-gate` enforces).
346
390
 
347
- These are advisory checks on the hub's own PRs (the front-half review PRs the bridge opens); they keep
391
+ These are advisory checks on the hub's own PRs (the Shape review PRs the verified ledger opens); they keep
348
392
  the hub's artifacts internally consistent. The hub never runs the code-repo `spec-link`/`build-test-lint`
349
393
  gates. Author the hub gate scripts under the hub's `checks/` following the same CI-agnostic-bash pattern.
350
394
 
@@ -352,12 +396,12 @@ The hub **does** run the verified-commits gate — `yad check --fix` installs `c
352
396
  plus a standalone workflow (`templates/github/yad-verified-commits.yml` →
353
397
  `.github/workflows/yad-verified-commits.yml`, or the GitLab fragment
354
398
  `templates/gitlab/yad-verified-commits.gitlab-ci.yml` → `.gitlab/ci/yad-verified-commits.yml` +
355
- its one include line) whenever `.sdlc/hub.json` has a platform with the bridge enabled. So the
356
- front-half review PRs are held to the same rule as code-repo PRs: signed, known authors only.
399
+ its one include line) whenever `.sdlc/hub.json` has a platform with a verified ledger. So the
400
+ Shape review PRs are held to the same rule as code-repo PRs: signed, known authors only.
357
401
 
358
402
  The hub **also** runs the three pattern gates (`commit-message`, `pr-title`, `pr-template`) with
359
403
  `--profile hub`. The pattern gates split by the PR/MR **head branch** (passed via `--head`): a
360
- `review/EP-*` head is a front-half review PR — Conventional-Commits commit subjects, a
404
+ `review/EP-*` head is a Shape review PR — Conventional-Commits commit subjects, a
361
405
  `review: <artifact> (EP-<slug>)` title, and the hub artifact-review template body; **any other head is
362
406
  a tooling/code change to the hub itself** and follows the `code` convention (a Conventional-Commits
363
407
  title + the code task template), so a PR that changes the hub's own workflows/checks can pass.
@@ -370,7 +414,7 @@ line). Code repos run the same three with `--profile code` inside the main `yad-
370
414
 
371
415
  Not a CI gate — a **harness hook**, and the only piece of yadflow that runs *inside* an agent's tool
372
416
  loop. It exists because of the gap #171 reported: `checks/ledger-guard.sh` is correct and blocking,
373
- but it speaks at CI time. An agent that hand-edits `epics/*/.sdlc/state.json` in bridge mode learns
417
+ but it speaks at CI time. An agent that hand-edits `epics/*/.sdlc/state.json` in verified mode learns
374
418
  twenty minutes later, from a FAIL with nothing connecting cause to effect, and by then the write must
375
419
  be reverted before the review PR/MR can go green.
376
420
 
@@ -410,7 +454,7 @@ logic is unit-tested (`cli/hook.mjs`, `cli/test.mjs`) instead of living in bash.
410
454
  - **Slugs are case-folded**, as the gate folds them. On a case-insensitive filesystem `epics/ep-x/…`
411
455
  and `epics/EP-X/…` are the same file, so a byte-exact compare would let a mutation be laundered as
412
456
  a creation.
413
- - Bridge-gated by the same `isBridgeHub` predicate the CLI and the wiring read (#186): without the
457
+ - Verified-only by the same `isVerifiedLedger` predicate the CLI and the wiring read (#186): without the
414
458
  bridge the ledger is locally owned, the hand-edit the authoring skills describe is correct, and
415
459
  nothing is wired or blocked.
416
460
 
@@ -461,7 +505,7 @@ The settings file is the team's, so the rules around that one entry are delibera
461
505
  `.claude` is the only IDE target wired: it is the only one with a defined hook protocol. Other
462
506
  targets get the script, and the contract above is what they would wire by hand.
463
507
 
464
- `yad doctor` reports the guard on a bridge hub, and distinguishes the three states that matter — it
508
+ `yad doctor` reports the guard on a verified hub, and distinguishes the three states that matter — it
465
509
  reads the same persisted `ideTargets` the wiring reads, so every gap it names is one the command it
466
510
  names can actually close:
467
511
 
@@ -1,26 +1,44 @@
1
1
  #!/usr/bin/env bash
2
2
  # build / test / lint gate (Phase 3 build plan §C).
3
3
  # Standard quality stage: lint, build, and tests that actually exercise behavior (not just pass).
4
- # Delegates to the repo's npm scripts so each repo owns the specifics.
4
+ # Delegates to the package manager named by package.json so each repo owns the specifics.
5
5
  set -euo pipefail
6
6
 
7
+ YAD_CHECKS_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
8
+ readonly YAD_CHECKS_DIR
9
+ # shellcheck source-path=SCRIPTDIR
10
+ # shellcheck source=package-manager.sh
11
+ source "$YAD_CHECKS_DIR/package-manager.sh"
12
+ # Two assignments on purpose: nested inside another $(...), a failing yad_package_manager_spec would
13
+ # be swallowed, and the gate would fall back to lockfile detection and PASS on a package.json it
14
+ # had just rejected. Each step fails the gate on its own under `set -e`.
15
+ package_manager_spec="$(yad_package_manager_spec)"
16
+ package_manager="$(yad_detect_package_manager "$package_manager_spec")"
17
+ # `npm`, `pnpm`, or `corepack npm` for a pinned npm — see yad_package_manager_command.
18
+ run_with_words="$(yad_package_manager_command "$package_manager_spec" "$package_manager")"
19
+ read -r -a run_with <<< "$run_with_words"
20
+
7
21
  echo "[build/test/lint] lint…"
8
- npm run --silent lint
22
+ "${run_with[@]}" run --silent lint
9
23
  echo "[build/test/lint] build…"
10
- npm run --silent build
24
+ "${run_with[@]}" run --silent build
11
25
 
12
26
  # Worker cap: when YAD_TEST_MAX_WORKERS is set AND the repo's test script is jest/vitest (the
13
27
  # runners that accept --maxWorkers), forward it to bound CI test concurrency. For any other runner
14
28
  # (node --test, mocha, …) it is a deliberate no-op so the gate never breaks on an unknown flag.
29
+ # npm needs the `--` separator to hand the flag to the script; pnpm (7+) forwards a literal `--` to
30
+ # the script instead, where jest reads `--maxWorkers=N` as a test-path pattern and finds no tests.
15
31
  extra=""
16
32
  if [ -n "${YAD_TEST_MAX_WORKERS:-}" ]; then
17
- case "$(npm pkg get scripts.test 2>/dev/null || true)" in
18
- *jest*|*vitest*) extra="-- --maxWorkers=${YAD_TEST_MAX_WORKERS}" ;;
33
+ case "$(node --input-type=module -e 'import fs from "node:fs"; const p=JSON.parse(fs.readFileSync("package.json", "utf8").replace(/^\uFEFF/, "")); process.stdout.write(p.scripts?.test || "")' 2>/dev/null || true)" in
34
+ *jest*|*vitest*)
35
+ if [ "$package_manager" = "pnpm" ]; then extra="--maxWorkers=${YAD_TEST_MAX_WORKERS}"; else extra="-- --maxWorkers=${YAD_TEST_MAX_WORKERS}"; fi
36
+ ;;
19
37
  esac
20
38
  fi
21
39
  echo "[build/test/lint] test…"
22
- # Intentional word-splitting: $extra is either empty or `-- --maxWorkers=N`.
40
+ # Intentional word-splitting: $extra is empty, `-- --maxWorkers=N` (npm), or `--maxWorkers=N` (pnpm).
23
41
  # shellcheck disable=SC2086
24
- npm run --silent test $extra
42
+ "${run_with[@]}" run --silent test $extra
25
43
 
26
44
  echo "PASS [build/test/lint]: lint, build, and tests all green."
@@ -3,7 +3,7 @@
3
3
  # is `shipped`. A SEALED epic's artifacts are the final, approved description of shipped behaviour — so
4
4
  # new behaviour must NOT be added to it; it belongs in a NEW threaded change-epic whose re-authored
5
5
  # stories/test-cases describe the change. This gate FAILs any non-maintenance commit whose owning epic
6
- # is sealed, forcing the front half to stay current (staleness becomes unshippable).
6
+ # is sealed, forcing Shape to stay current (staleness becomes unshippable).
7
7
  #
8
8
  # The owning epic lives in the PRODUCT repo (via specs/<story>/link.md `product-repo`). When it is not
9
9
  # reachable from CI, the seal cannot be read, so the commit PASSes with a note (degraded, fail-open here
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ # Deterministic CI dependency installation selected by package.json#packageManager.
3
+ set -euo pipefail
4
+
5
+ YAD_CHECKS_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
6
+ readonly YAD_CHECKS_DIR
7
+ # shellcheck source-path=SCRIPTDIR
8
+ # shellcheck source=package-manager.sh
9
+ source "$YAD_CHECKS_DIR/package-manager.sh"
10
+
11
+ package_manager_spec="$(yad_package_manager_spec)"
12
+ package_manager="$(yad_detect_package_manager "$package_manager_spec")"
13
+
14
+ case "$package_manager" in
15
+ npm)
16
+ if [ ! -f "$YAD_NPM_LOCKFILE" ] && [ ! -f "$YAD_NPM_SHRINKWRAP" ]; then
17
+ echo "FAIL [install-deps]: npm requires package-lock.json or npm-shrinkwrap.json." >&2
18
+ exit 1
19
+ fi
20
+ if [[ "$package_manager_spec" == npm@* ]]; then
21
+ yad_require_corepack
22
+ corepack enable
23
+ yad_corepack_prepare "$package_manager_spec"
24
+ # npm's shim is not enabled by Corepack, so dispatch it explicitly through Corepack — the same
25
+ # `corepack npm` the gate uses for lint/build/test, so package.json#packageManager selects the
26
+ # npm version end to end instead of the Node image's ambient npm binary.
27
+ corepack npm ci
28
+ else
29
+ npm ci
30
+ fi
31
+ ;;
32
+ pnpm)
33
+ if [ ! -f "$YAD_PNPM_LOCKFILE" ]; then
34
+ echo "FAIL [install-deps]: pnpm requires pnpm-lock.yaml." >&2
35
+ exit 1
36
+ fi
37
+ if [[ "$package_manager_spec" != pnpm@* ]]; then
38
+ echo "FAIL [install-deps]: pnpm CI requires an exact package.json#packageManager value (for example pnpm@9.15.0)." >&2
39
+ exit 1
40
+ fi
41
+ yad_require_corepack
42
+ corepack enable
43
+ yad_corepack_prepare "$package_manager_spec"
44
+ pnpm install --frozen-lockfile
45
+ ;;
46
+ esac
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # ledger-guard gate.
3
- # In BRIDGE mode the gate ledger is CI-owned: only the yad gate-sync bot may change the
3
+ # In verified mode the gate ledger is CI-owned: only the yad gate-sync bot may change the
4
4
  # machine-written gate-state files. A commit on a review PR by anyone else that modifies them is
5
5
  # rejected — the human keeps the artifact, CI keeps the ledger. This makes "CI is the sole writer of
6
6
  # the ledger" a mechanical guarantee instead of a convention.
@@ -20,21 +20,22 @@
20
20
  # distinguishes CI-generated commits. A spoofed-author commit that is not Verified is treated as a
21
21
  # human edit and rejected.
22
22
  #
23
- # Scope: enforced ONLY when the bridge is enabled — hub.json carries BOTH a `platform` and
24
- # `bridge_enabled` (or the legacy `bridge`) true, the same predicate `isBridge` (cli/gate.mjs) and
25
- # `hubActions` (cli/plan.mjs) apply. Without the bridge (file-only / non-bridge, or a platform-less
26
- # hub) humans legitimately write the ledger locally, so the gate is a no-op.
23
+ # Scope: enforced ONLY when the ledger is verified — hub.json carries BOTH a `platform` and either
24
+ # `ledger: "verified"` or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or the
25
+ # legacy `bridge`) true. That is the same predicate `isVerifiedLedger` (cli/manifest.mjs) and
26
+ # `hubActions` (cli/plan.mjs) apply. With a local ledger — or a platform-less hub — humans
27
+ # legitimately write the ledger themselves, so the gate is a no-op.
27
28
  #
28
29
  # Degradation: a base ref that cannot be resolved FAILs closed; no platform (cannot read the Verified
29
30
  # badge) WARNs and waives the signature half — the same stance verified-commits takes.
30
31
  set -euo pipefail
31
32
 
32
33
  # ---- bridge gate: only CI-owned ledgers are guarded -------------------------------------------
33
- # The predicate is BOTH a platform and the bridge flag, exactly as `isBridge` (cli/gate.mjs) and
34
+ # The predicate is BOTH a platform and the verified ledger flag, exactly as `isVerifiedLedger` (`cli/manifest.mjs`) and
34
35
  # `hubActions` (cli/plan.mjs) define it. Requiring the flag alone put this gate out of step with every
35
- # other bridge detector (issue #186): a hub carrying `bridge_enabled: true` with no `platform` would
36
+ # other ledger reader (issue #186): a hub carrying `bridge_enabled: true` with no `platform` would
36
37
  # have its human ledger commits rejected here while the CLI, reading the same file, called it
37
- # file-only and kept the LOCAL write path — no CI writer and no permitted human writer, so no gate
38
+ # local and kept the LOCAL write path — no CI writer and no permitted human writer, so no gate
38
39
  # could advance. Reachable through a stale install (platform set, script wired, platform later
39
40
  # nulled), not through `yad setup`, which derives both from one value.
40
41
  #
@@ -45,7 +46,7 @@ set -euo pipefail
45
46
  # Matched at the ROOT LEVEL only. The shared `default_branch` read below is depth-blind, and that is
46
47
  # survivable there — a false match yields a bogus branch name and the gate fails loudly. Here it is
47
48
  # not: a nested `"bridge": true` (say under `review`) would silently ENABLE this gate on a hub whose
48
- # `isBridge` is false, recreating the exact no-writer deadlock #186 is about, from the other side. So
49
+ # `isVerifiedLedger` is false, recreating the exact no-writer deadlock #186 is about, from the other side. So
49
50
  # the nesting is stripped rather than ignored: peel the outermost braces, then delete innermost
50
51
  # objects/arrays until none remain, leaving only root-level pairs to match against. Not a JSON parser
51
52
  # — a value containing a literal brace would confuse it — but hub.json is machine-written and the
@@ -67,9 +68,37 @@ done
67
68
  # SIGPIPE-under-pipefail problem this avoids).
68
69
  hub_str() { sed -nE "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"([^\"]*)\".*/\1/p" <<< "$HUB_ROOT"; }
69
70
  hub_true() { grep -Eq "\"$1\"[[:space:]]*:[[:space:]]*true" <<< "$HUB_ROOT"; }
71
+ # Is the key present with a STRING value at all, empty included? `hub_str` cannot answer this: it
72
+ # returns nothing both for a missing key and for an empty one.
73
+ hub_has_str() { grep -Eq "\"$1\"[[:space:]]*:[[:space:]]*\"" <<< "$HUB_ROOT"; }
70
74
 
71
- if [ ! -f "$HUB" ] || [ -z "$(hub_str platform)" ] || { ! hub_true bridge_enabled && ! hub_true bridge; }; then
72
- echo "PASS [ledger-guard]: bridge not enabled — the ledger is locally owned, nothing to guard."
75
+ # READ ORDER, identical to `isVerifiedLedger` (cli/manifest.mjs) — the two must never disagree:
76
+ # 1. `ledger`, if hub.json carries it — shape 2 and later. "verified" and nothing else.
77
+ # 2. otherwise the old booleans `bridge_enabled` (canonical) or `bridge` (older still).
78
+ # A platform is required either way, for the reason in the header.
79
+ #
80
+ # Step 2 is NOT dead weight. This script is committed inside the user's repo and refreshed by
81
+ # `yad update`, which is a separate act from `yad migrate` — so an un-migrated hub.json (no `ledger`
82
+ # key at all) will be read by this version of the script, and it has to keep saying "verified".
83
+ # cli/test-checks.mjs runs a table of hub.json variants through this script AND through the JS
84
+ # reader and asserts they agree on every row.
85
+ # Written as if/else rather than `cmd; verified=$?`: under the `set -e` above, a bare failing test
86
+ # would EXIT the script instead of recording a false — and exiting mid-guard is indistinguishable
87
+ # from passing, so the gate would silently stop guarding.
88
+ # PRESENCE of the key decides which branch runs, not whether its value is non-empty. `"ledger": ""`
89
+ # is a present key with a string value: the JS reader stops there and answers "local", so this must
90
+ # too. Testing `-n` instead sent an empty value down to the old booleans and the two readers gave
91
+ # OPPOSITE answers — the guard rejecting human writes while the CLI kept the local path open, which
92
+ # is the no-writer deadlock of #186 reached from a third direction.
93
+ verified=no
94
+ if hub_has_str ledger; then
95
+ if [ "$(hub_str ledger)" = "verified" ]; then verified=yes; fi
96
+ elif hub_true bridge_enabled || hub_true bridge; then
97
+ verified=yes
98
+ fi
99
+
100
+ if [ ! -f "$HUB" ] || [ -z "$(hub_str platform)" ] || [ "$verified" != yes ]; then
101
+ echo "PASS [ledger-guard]: the ledger is locally owned (ledger: local) — nothing to guard."
73
102
  exit 0
74
103
  fi
75
104
 
@@ -159,7 +188,7 @@ trusted_bot() {
159
188
  # A brand-new epic's ledger has no CI author. `gate ci` only ADVANCES an existing chain — it bails on
160
189
  # a missing state.json ("the review branch is cut from the default branch, so it should carry it") and
161
190
  # writes only at merge, on the default branch — and the engine itself reads a missing state.json as
162
- # "not seeded yet". `gate open` writes nothing in bridge mode, and `checkpoint` stages back-half
191
+ # "not seeded yet". `gate open` writes nothing in verified mode, and `checkpoint` stages Build
163
192
  # ledgers only. So the seed the authoring skills write (yad-epic / yad-change / yad-analysis /
164
193
  # yad-discovery / yad-stub) can reach the trunk ONLY through the first review PR/MR — the one place
165
194
  # this gate runs. Guarding it there makes the documented flow unshippable on a protected trunk, so an