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
@@ -22,7 +22,7 @@ trigger is a parameter, not a hardcoded human.
22
22
  - Operate on one epic: `{project-root}/epics/EP-<slug>/`. The Product level is the exception:
23
23
  `EP-foundation` lives at `{project-root}/foundation/` (its `.sdlc/` and `reviews/` are there).
24
24
  - State files: `.sdlc/state.json`, `.sdlc/approvals.json`, `.sdlc/comments.json`, and (when the verified ledger is
25
- used) `.sdlc/hub-prs.json`. Review records: `reviews/`.
25
+ used) `.sdlc/product-prs.json` (written together with its older name `hub-prs.json`). Review records: `reviews/`.
26
26
  - The artifact base name drops the extension (`epic.md` → `epic`; story `stories/...S01.md` → `stories-S01`).
27
27
 
28
28
  ## Inputs
@@ -32,7 +32,7 @@ trigger is a parameter, not a hardcoded human.
32
32
  `comment` / `approve` / `advance` are the `yad gate comment|approve|advance` commands (E112).
33
33
  - For `comment` / `approve`: the reviewer's platform login (or the name they give, when there is no
34
34
  platform). No role and no domain. Ask if not provided.
35
- - `sync` needs no reviewer input — it reads the platform PR/MR review state (via `yad-hub-bridge`).
35
+ - `sync` needs no reviewer input — it reads the platform PR/MR review state (via `yad-product-bridge`).
36
36
 
37
37
  ## On Activation
38
38
 
@@ -54,9 +54,8 @@ roles — there are no roles, and yadflow keeps no list of people:
54
54
  (`short`); never block on it. Why: the count of people errs high in the normal case. A commit is
55
55
  counted by its git name, an approval by its platform login, and yadflow never joins the two without
56
56
  exact evidence — so a two-person team can read as four. At four the cap lowers nothing, and an enforced
57
- contract gate would ask three approvals of a team with one person who is not the author. A later yadflow
58
- change turns the capped count on together with `yad gate lower --reason`, the way out, once the count
59
- is accurate.
57
+ contract gate would ask three approvals of a team with one person who is not the author. Enforcing the
58
+ capped count is parked (roadmap E108) until the count is accurate.
60
59
  - **Relay a warning line.** Under a team gate that has not passed, `yad gate status` and `yad gate sync`
61
60
  print `! may not be met: …` (today's one required approval may have nobody but the author to give
62
61
  it) or `! if the risk step were enforced: …` (a what-if about the extra approvals) when the count of
@@ -66,7 +65,7 @@ roles — there are no roles, and yadflow keeps no list of people:
66
65
  one person may work on the Product (E74). Relay it as a suggestion to run `yad mode team`; never switch
67
66
  the mode yourself, and never read it as a fault.
68
67
  - **When the people cannot be counted** (`active: null` — for example Product CI, which checks out only
69
- the hub, so the connected repos are not on disk): **no cap is computed or shown**, and the base holds as
68
+ the Product, so the connected repos are not on disk): **no cap is computed or shown**, and the base holds as
70
69
  always. `yad gate status` and `yad gate sync` print the arithmetic — read it from there rather than
71
70
  recomputing it.
72
71
  - **Touched domains** only name and label the review; they add no approvals. For a step with a risk
@@ -78,7 +77,7 @@ and nothing else.
78
77
 
79
78
  ### Step 2 — Dispatch on `action`
80
79
 
81
- > **Check the mode first — in verified mode you write nothing to the ledger.** Read `.sdlc/hub.json`:
80
+ > **Check the mode first — in verified mode you write nothing to the ledger.** Read `.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name):
82
81
  > **verified mode** is `platform` set AND `ledger: "verified"` — or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or legacy `bridge`) `true`. `ledger` wins whenever it is present. Under the verified ledger
83
82
  > the ledger is CI-owned — `ledger-guard` rejects any non-bot commit touching
84
83
  > `epics/*/.sdlc/{state,approvals,comments,product-prs,hub-prs}.json` or `epics/*/reviews/*.md` (and the
@@ -117,14 +116,14 @@ transition at merge.
117
116
 
118
117
  Do not advance.
119
118
 
120
- If `.sdlc/hub.json` has a non-null `platform` and `gh`/`glab` is authenticated, **`yad gate open` also
121
- opens the review PR/MR on the Product** (the recipe is `yad-hub-bridge action: open`; do not open a
119
+ If `.sdlc/product.json` (`.sdlc/hub.json` on an older Product) has a non-null `platform` and `gh`/`glab` is authenticated, **`yad gate open` also
120
+ opens the review PR/MR on the Product** (the recipe is `yad-product-bridge action: open`; do not open a
122
121
  second one), and you report the URL. This holds for a verified ledger and for a local one. The PR
123
122
  requests **no reviewers**; the command prints
124
123
  `no reviewers were requested — ask them on the PR itself`, so tell the author to ask people on the PR.
125
- The PR is recorded in `epics/<epic>/.sdlc/hub-prs.json` (`{step, artifact, platform, number, url, branch, lastSyncedAt}`):
124
+ The PR is recorded in `epics/<epic>/.sdlc/product-prs.json` and `hub-prs.json`, same bytes (`{step, artifact, platform, number, url, branch, lastSyncedAt}`):
126
125
  **by CI at merge** in verified mode (`ledger: "verified"`, or, before `yad migrate`, `bridge_enabled: true` /
127
- legacy `bridge: true` — `.sdlc/hub.json` is the only source the CLI reads, see `isVerifiedLedger` in
126
+ legacy `bridge: true` — `.sdlc/product.json` (else `.sdlc/hub.json`) is the only source the CLI reads, see `isVerifiedLedger` in
128
127
  `cli/manifest.mjs`), and by `yad gate open` itself on a local ledger. Never write it by hand (see `sync`
129
128
  below). With no platform, or when the PR cannot be opened on a local ledger, the step is still
130
129
  `in_review` locally — no error. (In verified mode a failed open writes nothing; open the PR by hand and
@@ -173,7 +172,7 @@ replaced. This is the **no-platform** path (E112): with a platform the approvals
173
172
  command refuses and names `yad gate sync`; on a verified ledger CI writes them at merge.
174
173
  `engagement` records whether the approval came through the [Review Companion](../yad-review-companion/SKILL.md)
175
174
  (a real trailer/cards/chat session = `verified`) or as a bare click (`none`). It is soft by default
176
- (both count; a bare approve draws a friendly nudge) and only gates when `hub.review.requireEngagement`
175
+ (both count; a bare approve draws a friendly nudge) and only gates when `review.requireEngagement` (Product settings)
177
176
  is on — see `references/gating.md`. The signal is gameable by design ("visible, not impossible").
178
177
  Then write/refresh `reviews/<artifact-base>--<YYYY-MM-DD>--approved.md` as a **named record** — the command
179
178
  writes the ledger only, never this file, so rewrite it after every `yad gate approve`. It has three
@@ -200,18 +199,18 @@ The command prints the verdict (Step 3) itself. Recording an approval does NOT a
200
199
  separate, explicit act (`advance`), the way approving a PR never merges it.
201
200
 
202
201
  **`sync`** — (the platform bridge input path) Pull the Product review PR/MR's review state into the ledger,
203
- then re-evaluate the rule (Step 3). Read the PR for this step from `.sdlc/hub-prs.json` and use
204
- `yad-hub-bridge`'s read recipes (`../yad-hub-bridge/references/bridge.md`) to fetch reviews + comments
202
+ then re-evaluate the rule (Step 3). Read the PR for this step from `.sdlc/product-prs.json` (`hub-prs.json` when that is absent) and use
203
+ `yad-product-bridge`'s read recipes (`../yad-product-bridge/references/bridge.md`) to fetch reviews + comments
205
204
  via the local user's `gh`/`glab`. For each:
206
205
  - the platform `login` is the name written — `approver` / `commenter` is the login itself. There is no
207
- lookup and no role (see `../yad-hub-bridge/references/login-roster.md`);
206
+ lookup and no role (see `../yad-product-bridge/references/login-roster.md`);
208
207
  - an `APPROVED` review / MR approval → append an `approved` record to `approvals.json` tagged
209
208
  `"source": "bridge"`; a `CHANGES_REQUESTED` review or an unresolved thread → write to
210
209
  `reviews/<artifact-base>--<YYYY-MM-DD>--comments.md` + `comments.json` (never an approval). A resolved
211
210
  thread, and a companion comment marked `<!-- yad:noblock -->`, is not written.
212
211
  **Idempotent:** upsert bridge approvals by `(step, approver)` — one record per person. An older record
213
212
  that still carries `role`/`domain` and names a person by their old roster name is recognised as
214
- `../yad-hub-bridge/references/login-roster.md` → "Older records" describes, and replaced by one
213
+ `../yad-product-bridge/references/login-roster.md` → "Older records" describes, and replaced by one
215
214
  login-named record that keeps its fingerprint. Every sync write (`yad gate sync`, `yad gate ci`) also
216
215
  records the login on the older records the roster can place for certain, on every step (same reference → "Recording the login on older records").
217
216
  A bridge approval records the platform's evidence — `approvedAt`, and on GitHub `commit`, `url` and
@@ -223,7 +222,7 @@ it. **Manual approvals (no `source` tag) are never touched.** For the architectu
223
222
  fingerprint a bridge approval is bound to is the contract surface, so a re-lock makes every approval of
224
223
  the old surface stale: it stays on disk and no longer counts (re-lock invalidates platform approvals
225
224
  too). Then refresh the `approved.md`
226
- record, set the PR ledger's `lastSyncedAt`, and **re-evaluate Step 3**. **Never hand-write either PR-ledger file.** It lives under two names while the rename settles — `product-prs.json` and `hub-prs.json` — and the engine writes both together. Writing one leaves the pair disagreeing, and `yad doctor` will report it. Use `yad gate`, which keeps them in step. Under the PR-driven CLI (`yad
225
+ record, set the PR ledger's `lastSyncedAt`, and **re-evaluate Step 3**. **Never hand-write either PR-ledger file.** It lives under two names until v5 — `product-prs.json` (read first) and `hub-prs.json` — and the engine writes both together. Writing one leaves the pair disagreeing, and then every yad command refuses (YAD-STATE-008) until `yad migrate --apply` settles it. Use `yad gate`, which keeps them in step. Under the PR-driven CLI (`yad
227
226
  gate sync`), `sync` advances the step when Step 3 passes on a **merged**, fully-resolved, approved PR
228
227
  (the merge is the human act); otherwise it records state and holds the step `in_review`.
229
228
 
@@ -239,7 +238,7 @@ The step may advance **iff ALL hold**:
239
238
  the base holds. Report the shortfall in the record; do not hold the step on it.
240
239
  This matches `gatePredicate`, which returns `rule: "count"`, the count as `gateRule`/`have`/`short`,
241
240
  `active`, and `cap` (null when `active` is null), and never puts the risk step in `missing`.
242
- `active: null` means no source could be read, never "nobody". With `hub.review.requireEngagement`
241
+ `active: null` means no source could be read, never "nobody". With `review.requireEngagement` (Product settings)
243
242
  on, only `verified` approvals count.
244
243
  2. The artifact has not changed since the latest approval round (no newer authored edit than the
245
244
  newest `approved` record). If it changed, approvals are stale → return to `comment`. For the
@@ -296,7 +295,7 @@ records this skill describes. The skill's
296
295
  job is the human half: presenting the artifact, helping the owner address comments, and narrating the
297
296
  gate. Local `yad gate sync` is advisory in verified mode (reads the platform, prints status, writes
298
297
  nothing); a human must never commit gate-state files (the `ledger-guard` check rejects it, and the
299
- `hooks/ledger-guard.sh` harness hook refuses an agent the edit up front, naming `yad gate open`
298
+ `hooks/ledger-guard.mjs` harness hook refuses an agent the edit up front, naming `yad gate open`
300
299
  instead — see `yad-checks`). The single
301
300
  exception is an epic's **seed** — no CI path can create a ledger, so a brand-new epic's `.sdlc/` rides
302
301
  its **first** review PR/MR, cut from the authoring branch (creation, not mutation, #162).
@@ -312,7 +311,7 @@ PR/MR are recorded and bound to the new content. `yad gate status` then shows th
312
311
  approvals are live against what is in the file today, and how many were revoked — instead of a `done`
313
312
  step whose approvals all belong to the version before the edit.
314
313
 
315
- The flow is **merge-driven** (wired by `yad-hub-bridge` `wire`): during review CI writes nothing — the
314
+ The flow is **merge-driven** (wired by `yad-product-bridge` `wire`): during review CI writes nothing — the
316
315
  platform PR/MR is the source of truth (native approvals + threads), and CI never touches the review
317
316
  branch (so an in-flight approval is never dismissed and required checks never strand). On the human
318
317
  **merge** CI re-reads approvals, advances the step, and flips the artifact `status:` on the **default
@@ -323,8 +322,8 @@ write path.
323
322
  ### Hard rules (build plan §1, §5)
324
323
  - **The merge click is the human approval act.** A Shape step advances only when a human merges the
325
324
  approved, fully-resolved review PR — or, on a Product with no platform, when a person runs
326
- `yad gate advance` — there is no machine-driven advance. A step `locked: true` may not
327
- be switched to `advance: auto`; refuse such a request.
325
+ `yad gate advance` — there is no machine-driven advance. A review gate is never `advance: auto`
326
+ (`yad dial` refuses it); refuse such a request.
328
327
  - **Approvals are revoked when the reviewed artifact changes.** `sync` re-hashes the artifact (the locked
329
328
  contract surface for architecture; every other file without its frontmatter `status:` line, which the
330
329
  gate and Build rewrite after review) and drops any approval bound to a stale hash, so a reviewer must
@@ -338,4 +337,4 @@ write path.
338
337
 
339
338
  ## Reference
340
339
  - Gating details and worked example: `references/gating.md`.
341
- - The platform PR/MR bridge (`open`/`sync` mechanics, read recipes, login attribution): `../yad-hub-bridge/SKILL.md`.
340
+ - The platform PR/MR bridge (`open`/`sync` mechanics, read recipes, login attribution): `../yad-product-bridge/SKILL.md`.
@@ -55,8 +55,8 @@ the risk step.
55
55
  counted by its git name, an approval by its platform login, and yadflow never joins the two without
56
56
  exact evidence. So an ordinary two-person team can read as **four**. At four the cap lowers nothing,
57
57
  and an enforced contract gate would ask three approvals of a team with one person who is not the author
58
- — a gate it could never pass. A later yadflow change turns the capped count on together with
59
- `yad gate lower --reason`, the way out of a gate that cannot be met, once the count is accurate.
58
+ — a gate it could never pass. Enforcing the capped count, with a way out for a gate that cannot be
59
+ met, is roadmap row E108, which is parked. Until then the count is advice only.
60
60
 
61
61
  **When a gate may not pass (E73).** `yad gate status` and `yad gate sync` print a warning line under a
62
62
  team review gate that has not passed yet, when the count of people suggests the gate may not pass. It is a
@@ -116,7 +116,7 @@ people, because the connected repos are not on disk there.
116
116
 
117
117
  **When the people cannot be counted** (`active: null` — a source could not be read), **no cap is
118
118
  computed or shown**, and the base holds as always. The engine never guesses a small number from an
119
- unknown. Product CI is usually this case: it checks out only the hub, so on a Product with connected
119
+ unknown. Product CI is usually this case: it checks out only the Product, so on a Product with connected
120
120
  repos the repos are not on disk.
121
121
 
122
122
  **Every cap is recorded.** When a **team** gate passes on its counted approvals while the cap lowered
@@ -132,7 +132,7 @@ Why count people: the rule names no person, no role and no step. A stored list o
132
132
  stale; repository access decides who can approve.
133
133
 
134
134
  Four surfaces print the count with its cap: `yad gate sync`, `yad gate status`, the generated
135
- review-PR body and `yad open-pr`. (`checks/risk-route.sh` and `checks/hub-route.sh` print the count
135
+ review-PR body and `yad open-pr`. (`checks/risk-route.sh` and `checks/product-route.sh` print the count
136
136
  without the cap — they cannot count people.) `yad gate sync` and `yad gate status` share one suffix; the review-PR body and `yad
137
137
  open-pr` word the cap their own way (below). The shared suffix appears only when the step has a risk
138
138
  step. It names the cap only when the cap lowered the ask, and always ends by saying what holds:
@@ -177,7 +177,7 @@ not be counted, nothing is suggested: `yad mode` and `yad doctor` (a warning) sa
177
177
  `verified` when it was recorded through the companion (a real trailer/cards/chat session), `none` for a
178
178
  bare UI click. By **default (soft)** both count: a bare approve still passes the gate but is recorded
179
179
  `none` and draws a friendly public @-mention nudge, so review *quality* is visible without blocking
180
- anyone. When `hub.review.requireEngagement: true`, only `verified` approvals are counted toward the
180
+ anyone. When `review.requireEngagement: true` in the Product settings, only `verified` approvals are counted toward the
181
181
  approvers above; when that leaves the gate short, it adds a line saying how many approvals did not count (a determined faker
182
182
  can still run an empty session — the signal is **gameable by design**; it raises the cost of a
183
183
  rubber-stamp and makes laziness visible, it does not prove a human read the artifact). Philosophy:
@@ -249,7 +249,7 @@ not "resolve to pass" — it ignores them). A reviewer's *genuine* concern is po
249
249
  marker and blocks normally, exactly as a `CHANGES_REQUESTED` or any unresolved human thread does.
250
250
 
251
251
  ## Platform-backed input (the verified ledger)
252
- When the Product has a platform (`.sdlc/hub.json`), reviewers can approve/comment
252
+ When the Product has a platform (`.sdlc/product.json`), reviewers can approve/comment
253
253
  on a real PR/MR instead — and only there: with a platform, `yad gate approve` / `comment` / `advance`
254
254
  refuse. The sync reads that platform
255
255
  state with the local user's own `gh`/`glab` and writes the **same**
@@ -262,26 +262,26 @@ unchanged**: it counts distinct approvers regardless of how they were recorded.
262
262
  - On PR/MR open the assignee is whoever opens it: `@me` on GitHub, and the login `glab` reports on GitLab.
263
263
  **No reviewers are requested** — the command prints `no reviewers were requested — ask them on the
264
264
  PR itself`. `domain:<repo>` labels for the touched domains are still applied. See
265
- `../yad-hub-bridge/references/login-roster.md`.
265
+ `../yad-product-bridge/references/login-roster.md`.
266
266
  - `sync` is idempotent (upsert by `(step, approver)`, one record per person; comment records by
267
267
  `(step, commenter, round)`, an unchanged round rewritten in place) and never touches **manual** approvals. An older bridge record that carries `role`/`domain` is
268
- recognised as `../yad-hub-bridge/references/login-roster.md` → "Older records" describes, and replaced
268
+ recognised as `../yad-product-bridge/references/login-roster.md` → "Older records" describes, and replaced
269
269
  by one login-named record that keeps its fingerprint. Every sync write (`yad gate sync`, `yad gate ci`)
270
270
  also records the login on the older records the roster can place for certain; only those it cannot
271
271
  place still need the roster on a later sync. A bridge approval records the
272
272
  platform's evidence (`approvedAt`, and on GitHub `commit`, `url`, `reviewId`) — for the record only. A revoked approval is superseded **while the step is open**; once
273
273
  the step is `done` its approvals are kept as the record of why it passed, and a re-sync only re-binds
274
- new ones (see `../yad-hub-bridge/references/bridge.md` → "Idempotent re-sync").
274
+ new ones (see `../yad-product-bridge/references/bridge.md` → "Idempotent re-sync").
275
275
  - The architecture+contract staleness rule applies to bridge approvals too: a re-lock changes the
276
276
  surface fingerprint, so bridge approvals of the old surface stop counting (they stay on disk as revoked).
277
- - No platform / no CLI → the gate runs local with no error. Detail: `../yad-hub-bridge/references/bridge.md`.
277
+ - No platform / no CLI → the gate runs local with no error. Detail: `../yad-product-bridge/references/bridge.md`.
278
278
 
279
279
  ## Why this shape
280
280
  - One approver who is not the author keeps review load low on a small team (design priority 2) while
281
281
  still requiring a second pair of eyes (priority 1, code quality / production safety).
282
282
  - The risk step asks for more people only where a change can break a shared surface
283
- (contract/auth/payments). It is advisory for now: the capacity cap (E72) is reported. A later change will
284
- enforce it together with `yad gate lower --reason`, so that a gate asking for more people than the team
285
- has will always have a way out. Until then E73 only warns when a gate may not pass.
283
+ (contract/auth/payments). It is advisory for now: the capacity cap (E72) is reported. Enforcing it, with a way
284
+ out for a gate that asks for more people than the team has, is roadmap row E108, which is parked. Until
285
+ then E73 only warns when a gate may not pass.
286
286
  - No stored list of people: it goes stale, and repository access already decides who can approve.
287
287
  - Everything is a file, so a future service can drive the same gate by writing the same records.
@@ -117,9 +117,9 @@ Walk the steps for `repo` starting at `from`/`currentStep`. For each step:
117
117
 
118
118
  **Commit the machine-written state.** After each iteration's writes (the trust-log shard in 2 and the
119
119
  build-state change in 4), run `yad checkpoint --push` from `{project-root}`. It commits *only* the
120
- `trust-log/` shards + `build-state/<story>.json` (never a Shape gate file) as one `chore(hub): …`
120
+ `trust-log/` shards + `build-state/<story>.json` (never a Shape gate file) as one `chore(product): …`
121
121
  audit-trail commit, and only ever on the default branch. It is a safe no-op when nothing changed, so
122
- call it every iteration — teammates don't review these machine writes, but CI and `yad status` on
122
+ call it every iteration — teammates don't review these machine writes, but CI and the `yad-status` skill on
123
123
  other machines must see current trust evidence. Never run it off the default branch (it will refuse):
124
124
  an unpushed or branch-stranded trust log leaves the run record `yad dial` shows out of date.
125
125
 
@@ -83,12 +83,12 @@ also the `build-log/` shards at engineer-review) — the shard dirs are what che
83
83
  way `git gc` folds loose objects later (`yad tidy up` folds finished shards into the folded
84
84
  `trust-log.json` / `build-log.json`) — plus any story `status:` flip (→ in-build/shipped) once that
85
85
  story has a build-log ship (#112) — as one
86
- `chore(hub): sync Build state — <epic>/<story> by @<login> [skip ci]`
86
+ `chore(product): sync Build state — <epic>/<story> by @<login> [skip ci]`
87
87
  audit-trail commit (`@<login>` when the platform login is known, else the git `user.name`, else `unknown`), on the default branch only,
88
88
  staging *only* those files by an explicit allowlist (never a Shape gate file — so `ledger-guard`
89
89
  never trips). It is idempotent (a no-op when nothing changed), so calling it after every transition —
90
90
  including a halt — is safe and keeps the shared trust evidence current for CI, teammates, and
91
- `yad status` on other machines. It refuses to run off the default branch (an unsigned `[skip ci]`
91
+ the `yad-status` skill on other machines. It refuses to run off the default branch (an unsigned `[skip ci]`
92
92
  commit inside a future PR range would fail `verified-commits` and strand the PR).
93
93
 
94
94
  ## Effective dial (the kill switch and gates always win)
@@ -95,7 +95,8 @@ to read. See `references/spec-handoff.md` for the exact file map and per-file de
95
95
  ### Step 6 — Write link.md back to the story
96
96
  Write `demo-repos/<repo>/specs/<story>/link.md` (template in `references/spec-handoff.md`) with
97
97
  frontmatter linking the spec back to the product repo: `story`, `epic`, `repo`, `feature-id`,
98
- `product-repo` (path), `contract-lock` (the hash **copied** from `contract-lock.json`, NOT recomputed
98
+ `product-repo` (path — when the code repo has `.sdlc/product-link.json`, write `../../<its path>`, the
99
+ place CI checks the Product out; the gates read that record first anyway), `contract-lock` (the hash **copied** from `contract-lock.json`, NOT recomputed
99
100
  in the code repo — or the literal `none` on a short-lane epic, which has no lock; never a made-up or
100
101
  empty hash, which the CI gate reads as a broken lock and fails), `speckit`
101
102
  (`installed | not-installed`), `generated` (date). This `link.md` plus the
@@ -94,19 +94,28 @@ The contract surface above is **referenced, not re-defined**. Any change to the
94
94
  back to the architecture gate in the product repo — it is never widened from this code repo.
95
95
  ```
96
96
 
97
+ `epic` must be the story ID's prefix (`EP-<slug>-S0N` → `EP-<slug>`); lineage-check, epic-open and
98
+ reconcile-debt-check FAIL on any other value (E118).
99
+
97
100
  `product-repo` is the one field CI resolves on disk (contract-check, lineage-check, epic-open,
98
101
  reconcile-debt all read it). Every gate resolves it the SAME way: an **absolute** path is used as-is; a
99
- **relative** path is joined to this `link.md`'s own directory, `specs/<story>/` — `../../` climbs out
100
- of the story dir and out of `specs/` to the code-repo root, so a Product checked out beside the code repo
102
+ **relative** path is joined to this `link.md`'s own directory, `specs/<story>/` — `../../` climbs out of
103
+ the story dir and out of `specs/` to the code-repo root, so a Product checked out beside the code repo
101
104
  is `../../../<product-dir>`. A relative value that only resolves from the **repo root** still works too
102
- (what contract-check historically did), so older `link.md` files keep gating. Write it as an
103
- **unquoted scalar** — `"..."` or `'...'` is taken literally, and a leading `~`/`$VAR` is never
104
- expanded. An unreachable path degrades the Product-reading gates to a PASS-with-note rather than failing
105
- them, and each one now says so in its output. **Reachable-but-lockless is a different case and does
106
- NOT degrade:** when the Product resolves and `epics/<epic>/.sdlc/` is there with no
107
- `contract-lock.json` in it, a claimed `Contract-Change: yes` FAILS, because nothing exists for the
108
- claim to be true of. That is the ordinary state of a short-lane epic, of a stub, and of a classic epic
109
- that has not reached its architecture gate yet.
105
+ (what contract-check historically did), so older `link.md` files keep gating. Write it as an **unquoted
106
+ scalar** — `"..."` or `'...'` is taken literally, and a leading `~`/`$VAR` is never expanded. An
107
+ unreachable path degrades the Product-reading gates to a PASS-with-note rather than failing them, and
108
+ each one now says so in its output. **CI reads `product-repo` from `link.md` as it stands on the base
109
+ branch** once one is merged (E117): a PR that changes it is read by the old value, and the new one
110
+ counts after the merge. **A merged `.sdlc/product-link.json` in the code repo comes before all of it
111
+ (E120):** its `path` (from the repo root, where CI checks the Product out) is what the gates read, and
112
+ `link.md`'s `product-repo` is only a fallback for a repo without one — so write `../../<that path>` here
113
+ when the record exists. A Product folder that the code repo itself tracks (a monorepo) is read as it
114
+ stands on the base commit, so a re-lock must merge before the slice change that pins it.
115
+ **Reachable-but-lockless is a different case and does NOT degrade:** when the Product resolves and
116
+ `epics/<epic>/.sdlc/` is there with no `contract-lock.json` in it, a claimed `Contract-Change: yes`
117
+ FAILS, because nothing exists for the claim to be true of. That is the ordinary state of a short-lane
118
+ epic, of a stub, and of a classic epic that has not reached its architecture gate yet.
110
119
 
111
120
  ## Do not re-invent the contract
112
121
 
@@ -113,7 +113,7 @@ As a <role>, I want <capability>, so that <outcome>.
113
113
  `repos` is the field the later build phase reads to know where to scaffold specs — set it precisely.
114
114
 
115
115
  ### Step 6 — Advance the authoring step (NOT the gate)
116
- **Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/hub.json`:
116
+ **Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name):
117
117
  **verified mode** is `platform` set AND `ledger: "verified"` — or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or legacy `bridge`) `true`. `ledger` wins whenever it is present.
118
118
 
119
119
  **verified mode — do NOT write `state.json`.** The ledger is CI-owned: the `ledger-guard` check rejects
@@ -145,7 +145,7 @@ reviewers — the team asks them on the review PR itself. When this gate
145
145
  passes the epic becomes **`ready-for-build`** — Build can start **and** the parallel
146
146
  **`test-cases`** track opens for the tester (`yad-test-cases`); the two run at the same time. **Never record
147
147
  approval here.** Shape steps do not auto-advance. When the Product has a platform, the gate opens a review
148
- PR on the Product (via `yad-hub-bridge`, with a `domain:<repo>` label per touched repo) and
148
+ PR on the Product (via `yad-product-bridge`, with a `domain:<repo>` label per touched repo) and
149
149
  `yad-review-gate action: sync` pulls platform approvals/comments into the ledger; otherwise the review
150
150
  is recorded local.
151
151
 
@@ -35,8 +35,21 @@ tree (branch + fast-forward). It writes **nothing** back to the registry.
35
35
  Run `yad repo sync [<repo>]`. For each target repo it performs, in order:
36
36
 
37
37
  ### Step 1 — Locate and validate the repo
38
- Resolve the local path from the registry. If the path is not a git repo (no readable `HEAD`), warn and
39
- skip — there is nothing to switch.
38
+ Resolve the local path from the registry. The registry is **shared content** — anyone who can push to
39
+ the Product wrote it — so the path is judged before any git runs (E81), the same way `yad join` and
40
+ `yad repo clone` judge it. The repo is **skipped with the reason** when:
41
+ - the path is outside the workspace (the Product folder's parent), or runs through a `.git` folder;
42
+ - nothing is there yet — **not cloned on this machine**: run `yad repo clone` to clone it;
43
+ - a folder is there but is not a checkout (a folder the Product commits could be shaped like a bare
44
+ git repo, and git run inside it would read its `config`, which can run a command). A folder inside a
45
+ checkout with no `.git` of its own (`apps/web` in a monorepo) is not refused (unless a link inside a
46
+ repo's tree or a folder holding an entry named `HEAD` is on its way — git can take such a folder for
47
+ a bare repo), but sync still skips it: switching its branch would switch the whole checkout, so sync
48
+ that checkout instead;
49
+ - a folder on the way, **inside a repo's tree**, is a link — the Product's `evil -> ../../..`. A link of
50
+ your own directly in the workspace folder (`ws/backend -> /src/backend`) is followed.
51
+
52
+ If the path is a checkout but `HEAD` is unreadable, warn and skip — there is nothing to switch.
40
53
 
41
54
  ### Step 2 — Skip a dirty working tree (never overwrite local work)
42
55
  If `git status --porcelain` is non-empty, the repo has uncommitted changes: **skip it and warn**
@@ -45,6 +58,9 @@ preserving local work is a hard rule.
45
58
 
46
59
  ### Step 3 — Determine the default branch
47
60
  Use the registry `default_branch`. If absent, fall back to the remote's `origin/HEAD`, else `main`.
61
+ A recorded name must be a valid `refs/heads/<name>` (`git check-ref-format`) and must not start with
62
+ `-` or `+`: the name is handed to `git fetch`, where `--upload-pack=<command>` would run a command.
63
+ Any other name skips the repo with the reason (fix it in `.sdlc/repos.json`).
48
64
 
49
65
  ### Step 4 — Fetch the latest
50
66
  If the repo has an `origin` remote: `git fetch origin <default_branch> --prune`. A local-only repo
@@ -65,7 +81,7 @@ so any repo whose `HEAD` now differs from its registry `syncedHead` has a **stal
65
81
  the command ends by pointing the human at `yad repo refresh` to repack (that is a separate human
66
82
  decision; this skill never repacks or writes the registry). After the repack + AI code-map
67
83
  regeneration, `yad repo refresh --push` publishes the refreshed code-maps + registry to the Product's
68
- default branch as a `chore(hub): sync code-context … [skip ci]` audit commit.
84
+ default branch as a `chore(product): sync code-context … [skip ci]` audit commit.
69
85
 
70
86
  ## Hard rules
71
87
 
@@ -164,7 +164,7 @@ Keep the `## Automation (<tool>)` section of `test-cases.md` in step with this f
164
164
  degraded (`testing: none`), do **not** write `test-links.json`.
165
165
 
166
166
  ### Step 5 — Advance the authoring step (NOT the gate)
167
- **Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/hub.json`:
167
+ **Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name):
168
168
  **verified mode** is `platform` set AND `ledger: "verified"` — or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or legacy `bridge`) `true`. `ledger` wins whenever it is present.
169
169
 
170
170
  **verified mode — do NOT write `state.json`.** The ledger is CI-owned: the `ledger-guard` check rejects
@@ -195,7 +195,7 @@ Report: the path to `test-cases.md`, the connected testing tool and what it prod
195
195
  Build may already be underway in parallel, and that the next action is **review** via
196
196
  `yad-review-gate` (base rule: 1 distinct approver, who should not be the author). **Never record approval here.** Shape steps do not
197
197
  auto-advance. When the Product has a platform, the gate opens a review
198
- PR on the Product (via `yad-hub-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments
198
+ PR on the Product (via `yad-product-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments
199
199
  into the ledger; otherwise the review is recorded local.
200
200
 
201
201
  ## Reference
@@ -184,7 +184,7 @@ Keep the `## Design (<tool>)` section of `ui-design.md` in step with this file.
184
184
  (`design: none`), do **not** write `design-links.json`.
185
185
 
186
186
  ### Step 5 — Advance the authoring step (NOT the gate)
187
- **Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/hub.json`:
187
+ **Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/product.json` (`.sdlc/hub.json` on an older Product that has only that name):
188
188
  **verified mode** is `platform` set AND `ledger: "verified"` — or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or legacy `bridge`) `true`. `ledger` wins whenever it is present.
189
189
 
190
190
  **verified mode — do NOT write `state.json`.** The ledger is CI-owned: the `ledger-guard` check rejects
@@ -216,7 +216,7 @@ Report: the paths to `ui-design.md` and `DESIGN.md`, whether Impeccable was used
216
216
  tool and what it produced (e.g. "Figma — 4 screens generated", the file URL + `design-links.json` path,
217
217
  or "no design tool — markdown-only"), and that the next action is **review** via `yad-review-gate` (base
218
218
  rule: 1 distinct approver, who should not be the author). **Never record approval here.** Shape steps do not auto-advance. When the Product has a platform, the gate opens a review PR on the
219
- Product (via `yad-hub-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments into
219
+ Product (via `yad-product-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments into
220
220
  the ledger; otherwise the review is recorded local.
221
221
 
222
222
  ## Reference
@@ -1,91 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ledger-guard CURSOR ADAPTER — the same guard, answered in the protocol Cursor requires (E11).
3
- #
4
- # Cursor's `preToolUse` is a PERMISSION hook, and its docs are explicit: for a permission hook,
5
- # "invalid JSON or a response that doesn't match the hook's schema blocks the action". Empty stdout is
6
- # invalid JSON. The plain `ledger-guard.sh` prints nothing when it allows — so wiring it directly
7
- # would have blocked EVERY file write in a verified project, which is the exact opposite of this
8
- # guard's fail-open design.
9
- #
10
- # This wrapper guarantees one thing: a permission answer on stdout, always, whatever happens below.
11
- #
12
- # allow {"permission":"allow"} exit 0
13
- # deny {"permission":"deny","user_message":"<reason>","agent_message":"<reason>"} exit 0
14
- #
15
- # A deny exits 0 because the JSON is the authoritative answer and exit 0 is what tells Cursor to read
16
- # it; exit 2 blocks too, but is documented as the code for "no JSON to read" and would discard the
17
- # reason — and naming the command that owns the transition is the whole point of speaking at edit time.
18
- #
19
- # It takes NO arguments, deliberately. Cursor's `hooks.json` holds one command STRING and does not
20
- # document whether it is run through a shell, so a command with a flag in it might never be split into
21
- # program and argument — a command not found, silently, on every call.
22
- set -uo pipefail
23
-
24
- # `CDPATH=` is load-bearing, and the Cursor entry is what makes it so: that entry invokes the script
25
- # by a RELATIVE path, so `${BASH_SOURCE[0]}` is relative and `cd` consults $CDPATH before the current
26
- # directory. An exported CDPATH in a login profile — an ordinary thing to have, and inherited by the
27
- # harness — would resolve `hooks` to some other tree entirely, and the guard would fail open on every
28
- # write for as long as that profile lived.
29
- #
30
- # The loop resolves a symlinked SCRIPT, which `cd -P` does not: `-P` resolves the directory path, and
31
- # a script reached through a link in another directory would otherwise look for its sibling beside the
32
- # LINK and not find it. `readlink` without `-f` because `-f` is not portable to macOS.
33
- _src="${BASH_SOURCE[0]}"
34
- while [ -L "$_src" ]; do
35
- _dir="$(CDPATH= cd -P -- "$(dirname -- "$_src")" && pwd)"
36
- _src="$(readlink -- "$_src")"
37
- case "$_src" in /*) ;; *) _src="$_dir/$_src" ;; esac
38
- done
39
- HOOK_DIR="$(CDPATH= cd -P -- "$(dirname -- "$_src")" && pwd)"
40
- ALLOW='{"permission":"allow"}'
41
-
42
- # Every failure below this line resolves to ALLOW, because this guard fails open by design and CI is
43
- # what fails closed. A wrapper that blocked when it could not reach `yad` would stop an agent editing
44
- # anything at all the moment an install went sideways.
45
- if [ ! -x "$HOOK_DIR/ledger-guard.sh" ]; then
46
- echo " • yad hook: $HOOK_DIR/ledger-guard.sh is missing or not executable — allowing (run \`yad check --fix\`)" >&2
47
- echo "$ALLOW"
48
- exit 0
49
- fi
50
-
51
- # stdout is CAPTURED (the JSON verdict), stderr is not (it flows through to Cursor's log). stdin is
52
- # inherited by the command substitution, so the tool-call payload still reaches the guard.
53
- raw="$("$HOOK_DIR/ledger-guard.sh" --format cursor)"
54
- rc=$?
55
-
56
- # THE VERDICT IS THE LAST NON-EMPTY LINE, not the whole of stdout, and it is recognised by containing
57
- # a `"permission"` key rather than by starting with one.
58
- #
59
- # Both halves of that matter. `yad`'s own top-level failure handler prints to STDOUT, so a usage error
60
- # or a crash banner can land in front of the JSON; a prefix test would then fail to recognise our own
61
- # deny and fall through to the allow below, turning a real refusal into a permitted write with nothing
62
- # on stderr to say so. And a containment test does not care what order the JSON's properties are in,
63
- # so the deny answer's shape on the other side of this language boundary is not silently load-bearing.
64
- verdict="$(printf '%s\n' "$raw" | awk 'NF { line = $0 } END { print line }')"
65
-
66
- # `ledger-guard.sh` has its own fail-open branches that exit 0 with EMPTY stdout — no `yad` on PATH,
67
- # an install it cannot resolve — and each one of those would otherwise reach Cursor as invalid JSON
68
- # and block the write.
69
- case "$verdict" in
70
- *'"permission"'*) echo "$verdict"; exit 0 ;;
71
- esac
72
-
73
- # EXIT 2 IS A DENY, even with nothing on stdout — and this branch is reached in a real, ordinary case.
74
- # `ledger-guard.sh` resolves `yad` from the Product's own `node_modules/yadflow` BEFORE `PATH`, so a
75
- # project pinned to a yadflow older than `--format` parses the flag, ignores it, and answers in the
76
- # exit protocol: exit 2, empty stdout. Falling through to the generic allow below would turn a real
77
- # refusal into a permitted write, and print "exited 2 — allowing" while doing it.
78
- #
79
- # The message is a FIXED literal with nothing interpolated. The reason the older `yad` printed has
80
- # already gone to stderr, where Cursor logs it; escaping arbitrary multi-line text into JSON in bash
81
- # is exactly the sort of thing that produces an off-schema response, and there is no need to risk it.
82
- if [ "$rc" -eq 2 ]; then
83
- echo '{"permission":"deny","user_message":"yad: this edit touches CI-owned gate state.","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."}'
84
- exit 0
85
- fi
86
-
87
- if [ "$rc" -ne 0 ]; then
88
- echo " • yad hook: ledger-guard.sh exited $rc — allowing (run \`yad doctor\` to check the install)" >&2
89
- fi
90
- echo "$ALLOW"
91
- exit 0
@@ -1,100 +0,0 @@
1
- #!/usr/bin/env bash
2
- # ledger-guard HARNESS HOOK — the local half of the CI gate of the same name (#171).
3
- #
4
- # The gate ledger is CI-owned in verified mode: `checks/ledger-guard.sh` rejects any non-bot commit
5
- # that changes `epics/*/.sdlc/{state,approvals,comments,product-prs,hub-prs}.json` or `epics/*/reviews/*.md`,
6
- # and the same files for the Foundation in `foundation/.sdlc/` and `foundation/reviews/` (E75).
7
- # This hook says so at the moment an agent tries the edit, instead of twenty minutes later in a
8
- # failed pipeline, and names the command that owns the transition (`yad gate open`).
9
- #
10
- # This file is only the ADAPTER. It locates `yad` and hands the tool-call payload to
11
- # `yad hook ledger-guard`, which holds the decision — so the wiring never hard-codes an install path
12
- # and the logic stays testable. The contract it passes through:
13
- #
14
- # stdin the harness's tool-call payload as JSON (optional)
15
- # exit 0 allow
16
- # exit 2 deny, reason on stderr
17
- #
18
- # Wired by `yad check --fix` for every harness that can refuse a write BEFORE it lands:
19
- # Claude Code a `PreToolUse` hook in `.claude/settings.json`, pointing straight at THIS script
20
- # Cursor a `preToolUse` hook in `.cursor/hooks.json`, pointing at `ledger-guard-cursor.sh`
21
- #
22
- # DO NOT WIRE THIS SCRIPT INTO CURSOR DIRECTLY, and check the same thing before wiring it into any
23
- # other harness by hand. Cursor's `preToolUse` is a PERMISSION hook: it wants a JSON verdict on
24
- # stdout, and treats an empty or off-schema answer as a refusal. This script prints NOTHING when it
25
- # allows — so wired there directly it would block every file write in the project, which is the
26
- # opposite of the fail-open stance below. `ledger-guard-cursor.sh` exists to speak that protocol.
27
- #
28
- # A harness that reads the EXIT CODE can use this script as it stands. A hook that only fires AFTER
29
- # the write — Cursor's `afterFileEdit`, for one — is deliberately not used: it could report the edit
30
- # but never refuse it, and reporting is what the CI gate already does.
31
- #
32
- # The project root is resolved from this script's own location below, so it does not matter which
33
- # variable the harness sets ($CLAUDE_PROJECT_DIR, $CURSOR_PROJECT_DIR) or where it runs the command
34
- # from; those variables appear only in the wiring, to point at this file.
35
- #
36
- # FAIL-OPEN: if no `yad` can be found, this ALLOWS and says why on stderr. A guardrail that blocked
37
- # every edit the moment an install went sideways would be worse than the problem. The CI gate fails
38
- # CLOSED and is what actually protects the ledger.
39
- set -uo pipefail
40
-
41
- # The Product root is this script's grandparent — hooks/ledger-guard.sh — so the resolution below does
42
- # not depend on the harness's working directory.
43
- # `CDPATH=` is load-bearing, and the Cursor entry is what makes it so: that entry invokes the script
44
- # by a RELATIVE path, so `${BASH_SOURCE[0]}` is relative and `cd` consults $CDPATH before the current
45
- # directory. An exported CDPATH in a login profile — an ordinary thing to have, and inherited by the
46
- # harness — would resolve `hooks` to some other tree entirely, and the guard would fail open on every
47
- # write for as long as that profile lived.
48
- #
49
- # The loop resolves a symlinked SCRIPT, which `cd -P` does not: `-P` resolves the directory path, and
50
- # a script reached through a link in another directory would otherwise look for its sibling beside the
51
- # LINK and not find it. `readlink` without `-f` because `-f` is not portable to macOS.
52
- _src="${BASH_SOURCE[0]}"
53
- while [ -L "$_src" ]; do
54
- _dir="$(CDPATH= cd -P -- "$(dirname -- "$_src")" && pwd)"
55
- _src="$(readlink -- "$_src")"
56
- case "$_src" in /*) ;; *) _src="$_dir/$_src" ;; esac
57
- done
58
- HOOK_DIR="$(CDPATH= cd -P -- "$(dirname -- "$_src")" && pwd)"
59
- HUB_ROOT="$(dirname -- "$HOOK_DIR")"
60
-
61
- # Resolution order, cheapest and most specific first: an explicit override, then the copy installed
62
- # in this Product, then whatever is on PATH, then a network-free npx. `--no-install` matters — a hook
63
- # runs on every tool call and must never pause an agent to download a package.
64
- CMD=()
65
- _yad_bin="${YAD_BIN:-}"
66
- if [ -n "${_yad_bin//[[:space:]]/}" ]; then
67
- # Split deliberately: YAD_BIN is commonly an interpreter + script ("node /path/to/yad.mjs").
68
- # The whitespace-stripped test above matters: a YAD_BIN of only spaces would leave CMD empty, and
69
- # macOS's bash 3.2 treats "${CMD[@]}" on an empty array as an unbound variable under `set -u` —
70
- # aborting the script with a confusing 127 instead of taking one of the branches below.
71
- read -r -a CMD <<< "$_yad_bin"
72
- elif [ -f "$HUB_ROOT/node_modules/yadflow/bin/yad.mjs" ] && command -v node >/dev/null 2>&1; then
73
- CMD=(node "$HUB_ROOT/node_modules/yadflow/bin/yad.mjs")
74
- elif command -v yad >/dev/null 2>&1; then
75
- CMD=(yad)
76
- elif command -v npx >/dev/null 2>&1; then
77
- CMD=(npx --no-install yadflow)
78
- else
79
- echo " • yad hook: no \`yad\` on PATH and none installed in $HUB_ROOT — allowing (install yadflow to re-arm the ledger guard)" >&2
80
- exit 0
81
- fi
82
-
83
- # Belt and braces for bash 3.2's empty-array-is-unbound rule: every branch above sets CMD, but an
84
- # unexpanded array under `set -u` would abort the script rather than allow, so check before using it.
85
- if [ "${#CMD[@]}" -eq 0 ]; then
86
- echo " • yad hook: could not resolve a \`yad\` to run — allowing" >&2
87
- exit 0
88
- fi
89
-
90
- # Run it rather than `exec`, so the exit code can be mapped. ONLY an explicit deny (2) blocks: a
91
- # `yad` that is present but cannot run — an `npx --no-install` with no yadflow to find, a crash, a
92
- # broken install — must not read as a refusal. Fail-open is the whole stance of this hook; the CI
93
- # gate is what fails closed.
94
- "${CMD[@]}" hook ledger-guard "$@"
95
- rc=$?
96
- [ "$rc" -eq 2 ] && exit 2
97
- if [ "$rc" -ne 0 ]; then
98
- echo " • yad hook: \`${CMD[*]} hook ledger-guard\` exited $rc — allowing (run \`yad doctor\` to check the install)" >&2
99
- fi
100
- exit 0