yadflow 3.18.1 → 4.0.0-next.1

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 (156) hide show
  1. package/CHANGELOG.md +355 -0
  2. package/README.md +79 -26
  3. package/bin/commands.mjs +41 -0
  4. package/bin/yad.mjs +437 -124
  5. package/cli/artifact-status.mjs +34 -15
  6. package/cli/checkpoint.mjs +69 -49
  7. package/cli/codeowners-command.mjs +170 -0
  8. package/cli/codeowners.mjs +397 -0
  9. package/cli/commit.mjs +13 -9
  10. package/cli/companion.mjs +2 -2
  11. package/cli/dial.mjs +183 -0
  12. package/cli/docs.mjs +88 -32
  13. package/cli/doctor.mjs +1472 -97
  14. package/cli/epic-state.mjs +3478 -232
  15. package/cli/epic.mjs +506 -0
  16. package/cli/errors.mjs +4 -1
  17. package/cli/gate.mjs +1002 -209
  18. package/cli/history.mjs +556 -0
  19. package/cli/hook.mjs +266 -55
  20. package/cli/hubcommit.mjs +6 -17
  21. package/cli/index-command.mjs +87 -0
  22. package/cli/ledger.mjs +57 -7
  23. package/cli/lib.mjs +184 -18
  24. package/cli/manifest.mjs +367 -56
  25. package/cli/migrate.mjs +726 -53
  26. package/cli/mode.mjs +170 -0
  27. package/cli/next.mjs +349 -90
  28. package/cli/openpr.mjs +191 -39
  29. package/cli/people.mjs +654 -0
  30. package/cli/plan.mjs +417 -132
  31. package/cli/platform.mjs +110 -129
  32. package/cli/product-index.mjs +287 -0
  33. package/cli/protection.mjs +706 -0
  34. package/cli/reconcile.mjs +38 -12
  35. package/cli/repo-publish.mjs +24 -26
  36. package/cli/repo.mjs +23 -14
  37. package/cli/report.mjs +21 -15
  38. package/cli/review.mjs +24 -27
  39. package/cli/riskmap-command.mjs +289 -0
  40. package/cli/riskmap.mjs +373 -0
  41. package/cli/setup.mjs +139 -287
  42. package/cli/ship.mjs +7 -6
  43. package/cli/skill.mjs +180 -0
  44. package/cli/skip.mjs +211 -30
  45. package/cli/thread.mjs +42 -17
  46. package/cli/tidy.mjs +20 -20
  47. package/cli/update-commit.mjs +22 -22
  48. package/cli/usage.mjs +115 -109
  49. package/package.json +3 -3
  50. package/skills/sdlc/config.yaml +166 -87
  51. package/skills/sdlc/module-help.csv +35 -35
  52. package/skills/yad-analysis/SKILL.md +125 -65
  53. package/skills/yad-architecture/SKILL.md +34 -23
  54. package/skills/yad-architecture/references/contract-format.md +10 -8
  55. package/skills/yad-backfill/SKILL.md +14 -8
  56. package/skills/yad-backfill/references/backfill.md +1 -1
  57. package/skills/yad-change/SKILL.md +127 -52
  58. package/skills/yad-change/references/triage.md +42 -28
  59. package/skills/yad-checks/SKILL.md +89 -45
  60. package/skills/yad-checks/references/check-gates.md +315 -92
  61. package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
  62. package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
  63. package/skills/yad-checks/templates/checks/contract-check.sh +58 -2
  64. package/skills/yad-checks/templates/checks/epic-open.sh +3 -3
  65. package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
  66. package/skills/yad-checks/templates/checks/ledger-guard.sh +94 -18
  67. package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
  68. package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
  69. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +4 -4
  70. package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
  71. package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
  72. package/skills/yad-checks/templates/github/yad-checks.yml +37 -5
  73. package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
  74. package/skills/yad-checks/templates/github/yad-update-guard.yml +3 -4
  75. package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
  76. package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
  77. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +22 -4
  78. package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
  79. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +4 -4
  80. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
  81. package/skills/yad-checks/templates/hooks/ledger-guard.sh +38 -7
  82. package/skills/yad-commit/SKILL.md +6 -6
  83. package/skills/yad-connect-design/SKILL.md +6 -6
  84. package/skills/yad-connect-design/references/design-context.md +1 -1
  85. package/skills/yad-connect-design/references/design-registry.md +2 -2
  86. package/skills/yad-connect-docs/SKILL.md +12 -12
  87. package/skills/yad-connect-docs/references/docs-registry.md +1 -1
  88. package/skills/yad-connect-learning/SKILL.md +5 -5
  89. package/skills/yad-connect-learning/references/learning-registry.md +2 -2
  90. package/skills/yad-connect-repos/SKILL.md +92 -54
  91. package/skills/yad-connect-repos/references/code-context.md +6 -6
  92. package/skills/yad-connect-repos/references/hub-config.md +68 -58
  93. package/skills/yad-connect-repos/references/repos-registry.md +10 -9
  94. package/skills/yad-connect-repos/references/risk-map.md +81 -0
  95. package/skills/yad-connect-testing/SKILL.md +6 -6
  96. package/skills/yad-connect-testing/references/testing-context.md +3 -4
  97. package/skills/yad-connect-testing/references/testing-registry.md +2 -2
  98. package/skills/yad-defects/SKILL.md +8 -8
  99. package/skills/yad-discovery/SKILL.md +130 -94
  100. package/skills/yad-discovery/references/discovery-schema.md +23 -7
  101. package/skills/yad-discovery/references/foundation-schema.md +374 -0
  102. package/skills/yad-docs/SKILL.md +16 -11
  103. package/skills/yad-docs/references/data-mapping.md +9 -7
  104. package/skills/yad-docs/templates/app/package-lock.json +3 -3
  105. package/skills/yad-docs-overview/SKILL.md +32 -17
  106. package/skills/yad-docs-overview/references/pipeline-model.md +47 -28
  107. package/skills/yad-docs-sync/SKILL.md +10 -5
  108. package/skills/yad-docs-sync/references/staleness.md +8 -7
  109. package/skills/yad-engineer-review/SKILL.md +88 -24
  110. package/skills/yad-engineer-review/references/ship-and-record.md +25 -16
  111. package/skills/yad-epic/SKILL.md +178 -100
  112. package/skills/yad-epic/references/state-schema.md +626 -117
  113. package/skills/yad-hub-bridge/SKILL.md +66 -48
  114. package/skills/yad-hub-bridge/references/bridge.md +110 -83
  115. package/skills/yad-hub-bridge/references/login-roster.md +163 -70
  116. package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
  117. package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
  118. package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
  119. package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +22 -12
  120. package/skills/yad-implement/SKILL.md +29 -15
  121. package/skills/yad-implement/references/implement-conventions.md +2 -2
  122. package/skills/yad-learn/SKILL.md +9 -9
  123. package/skills/yad-learn/references/learning-state.md +2 -2
  124. package/skills/yad-open-pr/SKILL.md +64 -29
  125. package/skills/yad-pair-review/SKILL.md +18 -16
  126. package/skills/yad-pair-review/references/session-state.md +4 -4
  127. package/skills/yad-pr-template/SKILL.md +48 -27
  128. package/skills/yad-pr-template/references/risk-routing.md +97 -24
  129. package/skills/yad-pr-template/templates/checks/pr-template.sh +37 -15
  130. package/skills/yad-pr-template/templates/checks/pr-title.sh +27 -13
  131. package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
  132. package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
  133. package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
  134. package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +15 -14
  135. package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +15 -13
  136. package/skills/yad-reconcile/SKILL.md +3 -3
  137. package/skills/yad-report/SKILL.md +5 -5
  138. package/skills/yad-review-companion/SKILL.md +12 -9
  139. package/skills/yad-review-gate/SKILL.md +198 -79
  140. package/skills/yad-review-gate/references/gating.md +230 -54
  141. package/skills/yad-run/SKILL.md +86 -56
  142. package/skills/yad-run/references/run-loop.md +67 -45
  143. package/skills/yad-ship/SKILL.md +18 -14
  144. package/skills/yad-spec/SKILL.md +31 -17
  145. package/skills/yad-spec/references/spec-handoff.md +17 -5
  146. package/skills/yad-status/SKILL.md +114 -56
  147. package/skills/yad-stories/SKILL.md +42 -27
  148. package/skills/yad-stories/references/story-schema.md +10 -9
  149. package/skills/yad-stub/SKILL.md +59 -48
  150. package/skills/yad-sync-repos/SKILL.md +3 -3
  151. package/skills/yad-test-cases/SKILL.md +37 -30
  152. package/skills/yad-test-cases/references/test-cases-schema.md +8 -5
  153. package/skills/yad-timeline/SKILL.md +8 -7
  154. package/skills/yad-ui/SKILL.md +46 -25
  155. package/cli/roster.mjs +0 -164
  156. package/skills/sdlc/install.sh +0 -68
package/cli/manifest.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // The single source of truth for what a set-up SDLC project should contain.
2
2
  // Drives setup (install from), update (re-sync), and check (diff against).
3
- // Keep the skill list here in sync with skills/sdlc/install.sh.
4
- import { readFileSync } from 'node:fs';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import path from 'node:path';
5
5
 
6
6
  // Read the version from package.json (the one source of truth) so it always
7
7
  // tracks the semantic-release-managed version — never a hardcoded constant
@@ -21,7 +21,7 @@ export const UPSTREAM_REPO =
21
21
  (pkg.bugs?.url || '').match(/github\.com\/([^/]+\/[^/]+?)(?:\/issues)?\/?$/i)?.[1]
22
22
  || 'abdelrahmannasr/yadflow';
23
23
 
24
- // The hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
24
+ // The hand-authored yad-* skills.
25
25
  export const SKILLS = [
26
26
  'yad-discovery',
27
27
  'yad-analysis',
@@ -104,7 +104,7 @@ export const LEGACY_REPO_FILES = {
104
104
  github: { '.github/workflows/sdlc-checks.yml': '.github/workflows/yad-checks.yml' },
105
105
  gitlab: { '.gitlab/ci/sdlc-checks.yml': '.gitlab/ci/yad-checks.yml' },
106
106
  };
107
- export const LEGACY_HUB_FILES = {
107
+ export const LEGACY_PRODUCT_FILES = {
108
108
  github: {
109
109
  '.github/workflows/sdlc-gate-sync.yml': '.github/workflows/yad-gate-sync.yml',
110
110
  '.github/workflows/sdlc-verified-commits.yml': '.github/workflows/yad-verified-commits.yml',
@@ -116,18 +116,65 @@ export const LEGACY_HUB_FILES = {
116
116
  };
117
117
 
118
118
  // IDE install targets (relative to the target project root).
119
- export const IDE_FOLDER_TARGETS = Object.freeze(['.claude', '.agents', '.zencoder']); // <ide>/skills/<skill>/ (folder copy)
119
+ export const IDE_FOLDER_TARGETS = Object.freeze(['.claude', '.agents', '.cursor', '.gemini', '.zencoder']); // <ide>/skills/<skill>/ (folder copy)
120
120
  export const IDE_OPENCODE_TARGET = '.opencode';
121
121
  export const IDE_TARGETS = Object.freeze([...IDE_FOLDER_TARGETS, IDE_OPENCODE_TARGET]);
122
122
  export const IDE_OPENCODE_DIR = `${IDE_OPENCODE_TARGET}/commands`; // <skill>.md (flat SKILL.md copy)
123
123
 
124
- // Module registration files copied from skills/sdlc/ into _bmad/sdlc/.
125
- export const MODULE_FILES = ['config.yaml', 'module-help.csv'];
124
+ // WHICH AGENT READS WHICH TARGET (E11). A target is a DIRECTORY, never an agent name — that is the
125
+ // closed decision, and it is the reason nothing here needs a migration: the stamped value in
126
+ // `.sdlc/cli-version.json` keeps meaning exactly what it always meant.
127
+ //
128
+ // This table exists because the mapping is not one-to-one and the docs were wrong about it. `SKILL.md`
129
+ // became a cross-agent format, and `.agents/skills/` the directory several agents agreed to read, so
130
+ // yadflow has been installing for Codex, Gemini CLI, Cursor and Copilot since long before this row —
131
+ // it just never said so, and a Codex user reading "Claude Code (plus .agents…)" had no way to know.
132
+ //
133
+ // Checked against each agent's own documentation on 2026-09-16, NOT from memory; every entry below is
134
+ // a directory that agent's docs name. Re-check before adding one — a guessed path installs in silence
135
+ // and is never read.
136
+ //
137
+ // .claude Claude Code. Cursor also reads it for compatibility.
138
+ // .agents the cross-agent directory: Codex CLI (repo root), Gemini CLI (alias), Cursor, Copilot.
139
+ // .cursor Cursor's own. Redundant beside `.agents`, offered for a Cursor-only project.
140
+ // .gemini Gemini CLI's own. Redundant beside `.agents`, same reason.
141
+ // .zencoder Zencoder.
142
+ // .opencode opencode — a FLAT `commands/<skill>.md` copy, not a folder (see IDE_OPENCODE_DIR).
143
+ //
144
+ // Used by the `yad setup` prompt, `yad doctor` and the docs, so the list a user is shown and the list
145
+ // the installer honours can never drift apart.
146
+ export const IDE_AGENTS = Object.freeze({
147
+ '.claude': Object.freeze(['Claude Code', 'Cursor']),
148
+ '.agents': Object.freeze(['Codex CLI', 'Gemini CLI', 'Cursor', 'GitHub Copilot']),
149
+ '.cursor': Object.freeze(['Cursor']),
150
+ '.gemini': Object.freeze(['Gemini CLI']),
151
+ '.zencoder': Object.freeze(['Zencoder']),
152
+ '.opencode': Object.freeze(['opencode']),
153
+ });
154
+
155
+ // What a FRESH `yad setup` offers when the project has no agent directory yet: Claude Code plus the
156
+ // cross-agent directory, which together cover every agent named above. The cost is honest and small —
157
+ // the skills are written twice, once per directory.
158
+ //
159
+ // NOT the same value as IDE_RECOVERY_TARGET below, and the difference is deliberate. This is a CHOICE
160
+ // offered to someone setting up; that one is what a project falls back to when its stamp is missing or
161
+ // unreadable. Installing `.agents` into an existing `.claude`-only project because its stamp went
162
+ // sideways would write forty skill folders the team never asked for, on a recovery path they did not
163
+ // choose to walk. A recovery restores the minimum; it does not upgrade anyone.
164
+ export const DEFAULT_IDE_TARGETS = Object.freeze(['.claude', '.agents']);
165
+ export const IDE_RECOVERY_TARGET = '.claude';
166
+
167
+ // The module config, copied from skills/sdlc/config.yaml into the project, where the skills read it (E3).
168
+ // Until E3 it went to `_bmad/sdlc/`, beside `module-help.csv`, because yadflow was packaged as a BMAD
169
+ // module. Only BMAD's own help menu read the installed `module-help.csv`, so that file is not installed
170
+ // at all now; its source stays in skills/sdlc/, where yad-docs-overview reads it. An existing
171
+ // `_bmad/sdlc/` is left where it is, and `yad doctor` names it.
172
+ export const MODULE_CONFIG = '.sdlc/config.yaml';
126
173
 
127
174
  // Supported design-tool adapters (mirrors skills/sdlc/config.yaml `design.tools`); `DESIGN_PRIMARY` is
128
175
  // the fallback `registerDesign`/setup use when an unknown tool is named, and `none` is the explicit
129
176
  // markdown-only choice. (doctor does NOT fall back — an unknown tool there is a hard YAD-CFG-002 fail,
130
- // mirroring how registerRepo falls back on platform while doctor fails on an unknown hub platform.)
177
+ // mirroring how registerRepo falls back on platform while doctor fails on an unknown Product platform.)
131
178
  export const DESIGN_TOOLS = ['figma', 'pencil'];
132
179
  export const DESIGN_PRIMARY = 'figma';
133
180
 
@@ -147,44 +194,194 @@ export const TESTING_PRIMARY = 'playwright';
147
194
  export const LEARNING_TOOLS = ['deeptutor'];
148
195
  export const LEARNING_PRIMARY = 'deeptutor';
149
196
 
150
- // The shape (schema version) every file the engine writes declares, as `"schemaVersion": 1`.
197
+ // The shape (schema version) every file the engine writes declares, as `"schemaVersion": <n>`.
151
198
  //
152
199
  // Rule 1 of the change-safety rules (docs/roadmap-idea-1.md, Part 2): every file states its shape,
153
200
  // and a file with no version counts as 1.
154
201
  //
155
- // Today the stamp is written and read back, and nothing yet acts on it: raising this number would move
156
- // what new files say without upgrading existing ones. The two halves that make it usable are the next
157
- // tasks on the roadmap — `yad migrate` (E14), which moves a project from one shape to the next, and a
158
- // `yad doctor` report (E16) for a project whose files disagree with the engine. Do NOT raise this
159
- // number before both exist.
202
+ // Raising this number is not a one-line change. Three things move together, or a project is left
203
+ // holding files it cannot upgrade:
204
+ // 1. a step appended to MIGRATIONS (cli/migrate.mjs) taking a file from the old shape to the new
205
+ // one — the chain from 1 upwards must have no gap, which cli/test-migrate.mjs asserts;
206
+ // 2. `docs/migrations/shape-<n>.md`, which scripts/shape-guide-check.sh REFUSES to release without;
207
+ // 3. every writer of the changed file taught to write the new shape's fields.
208
+ // `yad doctor` reports a project whose files disagree with this number, and `yad migrate` is what
209
+ // closes the gap.
210
+ //
211
+ // 2 — `.sdlc/hub.json` records who writes the ledger as `ledger: verified | local` (E104).
212
+ // 3 — the Product's settings gain `.sdlc/product.json` beside `.sdlc/hub.json`, and a roster entry's
213
+ // product-level roles gain a `product` spelling beside `hub` (E30).
214
+ // 4 — every step declares `driver` beside `assistance` and `advance` beside `automation` (E28).
215
+ // 5 — every epic's `state.json` records its work-item `type`, and `epic.md` gains `type:` beside
216
+ // `kind:` (E21). `kind:` is still the name that is READ — see workItemType in epic-state.mjs.
217
+ // 6 — every epic's `state.json` records the lifecycle `profile` its chain came from (E17). The value
218
+ // is one of LIFECYCLE_PROFILES in epic-state.mjs, and for an existing epic it is DERIVED from the
219
+ // chain it already carries — never defaulted, because guessing a route would tell `yad epic new`
220
+ // and `yad doctor` a chain is on a route nobody chose.
221
+ // 7 — every step's `status` is one of the step-state model's words (E38, STEP_STATES in
222
+ // epic-state.mjs). `blocked` stops meaning "not started" and starts meaning "cannot proceed, not
223
+ // our choice"; the old spelling of the first is rewritten to `todo`, a skip becomes
224
+ // `status: skipped` and an inherited step `status: satisfied`, each with a `record` saying why.
225
+ // THE FIRST SHAPE THAT CHANGES A VALUE IN PLACE rather than adding a key beside an old one — so
226
+ // it is the first that an older CLI cannot read correctly, which docs/migrations/shape-7.md says
227
+ // in as many words. Every legacy field is kept beside the new one (rule 3).
228
+ // 8 — the Product level lives in `foundation/` as the Foundation, id `EP-foundation` (E75). On a
229
+ // local ledger `yad migrate` MOVES `epics/EP-discovery/` there and relabels its step ids; no field
230
+ // of any other file changes. On a verified project the gate bot makes the same
231
+ // move (`yad gate ci`); until it lands, the old spelling is still read.
232
+ // docs/migrations/shape-8.md.
233
+ // 9 — an approval's fingerprint leaves out the artifact's frontmatter `status:` line (`reviewedSha` in
234
+ // epic-state.mjs). No field is added, renamed or moved, and no file is rewritten: what changes is
235
+ // the VALUE the gate writes into `artifactHash` on a new approval. It is still a shape, and a
236
+ // breaking one, because an older yadflow reads that value as stale — so it has to see a newer
237
+ // shape and say so. This release reads both the new value and every form older ones recorded.
238
+ // docs/migrations/shape-9.md.
239
+ // 10 — a deferred step can be put back after the chain has built past it, and can carry `debt: true`
240
+ // (E41). One optional key is added to a step in `state.json`, written only by `yad defer --debt`,
241
+ // and nothing on disk is rewritten. It is a breaking shape because an older yadflow cannot read a
242
+ // step re-opened behind finished work: it names that step the blocker of the work, and passing its
243
+ // review re-opens the step after it. docs/migrations/shape-10.md.
160
244
  //
161
245
  // Deliberately NOT the same thing as `VERSION` above. That is which release of the CLI you are
162
246
  // running and moves on every publish; this is what the files on disk look like and moves only when
163
247
  // their shape actually changes.
164
- export const SCHEMA_VERSION = 1;
248
+
249
+ export const SCHEMA_VERSION = 10;
165
250
 
166
251
  // Project-level files setup produces (used by `check` to spot missing setup).
167
252
  export const PROJECT_FILES = {
168
253
  reposRegistry: '.sdlc/repos.json',
254
+ // The product's own settings. `product.json` is the name from shape 3 onward; `hub.json` is what
255
+ // it was called before, and it is NOT dead — see MIRRORED_FILES below.
256
+ productConfig: '.sdlc/product.json',
169
257
  hubConfig: '.sdlc/hub.json',
170
258
  designConfig: '.sdlc/design.json',
171
259
  testingConfig: '.sdlc/testing.json',
172
260
  learningConfig: '.sdlc/learning.json',
173
261
  docsConfig: '.sdlc/docs.json',
262
+ // Which skill runs which step, when the project does not want the engine's default (E6). Absent is
263
+ // the normal case and means "every step uses the skill the catalogue names" — like design.json,
264
+ // which is absent on a markdown-only project.
265
+ skillsConfig: '.sdlc/skills.json',
266
+ // The kill switch and the Shape steps a team has set to `advance: auto` (E34). Absent is the normal
267
+ // case: the switch is off and every Shape step is `human`. A project file, not the module config the
268
+ // switch lived in before (`_bmad/sdlc/config.yaml` then, `.sdlc/config.yaml` since E3) — that one is
269
+ // hash-managed by `yad update`, so flipping it there marked the file modified and every later update
270
+ // skipped it.
271
+ automationConfig: '.sdlc/automation.json',
174
272
  version: '.sdlc/cli-version.json',
175
273
  };
176
274
 
177
- // Bridge mode: a platform AND the gate-sync CI explicitly enabled (the canonical `bridge_enabled`,
178
- // or the older `bridge`). ONLY then is CI the sole ledger writer — so `gate open`/`sync` stay
179
- // hands-off, `hubActions` wires the hub CI, and the ledger guards (the `ledger-guard` check gate and
180
- // the `yad hook ledger-guard` harness hook) are live. A platform without the bridge keeps the local
181
- // write path, or reviews could never advance.
275
+ // ---- files that changed NAME in shape 3 --------------------------------------------------------
276
+ //
277
+ // `hub` became `Product`, so `.sdlc/hub.json` became `.sdlc/product.json`. A field rename is easy;
278
+ // a FILE rename is not, because a file is opened by name from outside this codebase:
279
+ //
280
+ // - `templates/checks/ledger-guard.sh` is committed inside the USER's repo and opens
281
+ // `.sdlc/hub.json` by that literal path. It is refreshed by `yad update`, which is a separate act
282
+ // from `yad migrate` with no ordering between them.
283
+ // - So a project WILL exist that has been migrated but not updated. Its guard would open a path
284
+ // that no longer exists, read no platform, conclude the ledger is local, and stop rejecting human
285
+ // commits to it — the audit trail disarmed by an upgrade.
286
+ //
287
+ // Hence: for one whole major version BOTH files exist, and the OLD name is the one that is READ.
288
+ // `product.json` is written on every save so that it is there, correct, and ready — but nothing
289
+ // depends on it yet. `productConfigPath` picks the old name whenever it exists and falls back to the
290
+ // new one, so a project holding either name alone still works.
291
+ //
292
+ // Reading the new name first is the tempting version and it is wrong: the moment two names exist and
293
+ // the new one wins, everything that writes the old one — the guard above, a script someone wrote,
294
+ // a person editing the file they know — is silently ignored.
295
+ //
296
+ // The ladder is add, then switch, then remove:
297
+ // this major the new name appears and is maintained; the old one is still read
298
+ // next major the new name becomes the one read, and `yad doctor` warns about the old
299
+ // after that the old name is deleted
300
+ //
301
+ // This costs a duplicated file on disk for two releases. That is the price of not silently disarming
302
+ // a safety gate in somebody else's repository, and it is worth paying.
303
+ export const MIRRORED_FILES = [
304
+ { canonical: PROJECT_FILES.productConfig, legacy: PROJECT_FILES.hubConfig },
305
+ ];
306
+
307
+ // Which of the two names to READ.
308
+ //
309
+ // The OLD name wins while it exists, and that is deliberate. Renaming a file across an ecosystem
310
+ // takes three releases, not one:
311
+ //
312
+ // this major the new name appears and is written on every save. The OLD name is still the one
313
+ // that counts, so everything that already reads it keeps working — the ledger-guard
314
+ // committed in the user's repo, a script somebody wrote, a person editing the file
315
+ // they know. Nothing can be silently ignored, because the file everyone knows is
316
+ // still authoritative.
317
+ // next major the new name becomes authoritative and `yad doctor` warns about the old one.
318
+ // the one after that the old name is deleted.
182
319
  //
183
- // ONE definition, imported by every JS caller. Copies that drift are how #186 happened — a hub that
184
- // one reader called bridge and another called file-only had no permitted ledger writer at all.
185
- // `templates/checks/ledger-guard.sh` re-implements it in bash because the check gates are standalone
186
- // by design; that copy is the only one, and its header says so.
187
- export const isBridgeHub = (hub) => !!(hub?.platform && (hub.bridge_enabled === true || hub.bridge === true));
320
+ // Reading the NEW name first this early looks tidier and is a trap: the moment two names exist and
321
+ // the new one wins, anyone who edits the old one — including our own fixtures, which is how this was
322
+ // found — has their change silently ignored. `yad doctor` reports the two copies drifting apart, so
323
+ // a project that gets into that state is told, rather than left to wonder.
324
+ export const productConfigPath = (root) => {
325
+ const legacy = path.join(root, PROJECT_FILES.hubConfig);
326
+ return existsSync(legacy) ? legacy : path.join(root, PROJECT_FILES.productConfig);
327
+ };
328
+
329
+ // Same rule for a renamed file inside an epic's ledger.
330
+ export const preferring = (canonical, legacy) => (existsSync(legacy) ? legacy : canonical);
331
+
332
+ // Who writes the ledger. Two values, and the switch lives in `.sdlc/hub.json`:
333
+ //
334
+ // ledger: "verified" CI only, with a platform-Verified signature. A local `gate open` is
335
+ // advisory and writes nothing; `ledger-guard` rejects any non-bot commit.
336
+ // ledger: "local" your machine. Works offline, no CI needed, guarded by nothing.
337
+ //
338
+ // `verified` is the old "bridge mode" renamed. The old name described a mechanism; this one
339
+ // describes what you get, and it is the word the platform shows next to the commits.
340
+ //
341
+ // READ ORDER, and it matters (rule 2 — read old, write new):
342
+ // 1. `ledger`, if the file carries it — shape 2 and later.
343
+ // 2. otherwise the old booleans `bridge_enabled` (canonical) or `bridge` (older still).
344
+ // A platform is required either way. Without one there is no Verified badge to read, so CI cannot
345
+ // be the sole writer and the local path has to stay open — otherwise a Product has no permitted writer
346
+ // at all and no gate can ever advance (issue #186).
347
+ //
348
+ // ONE definition, imported by every JS caller. `templates/checks/ledger-guard.sh` re-implements the
349
+ // SAME order in bash because the check gates are standalone by design; that copy is the only one,
350
+ // its header says so, and cli/test-checks.mjs runs a table of hub.json variants through both and
351
+ // asserts they agree on every row. Three keys is three ways for two readers to drift.
352
+ export const isVerifiedLedger = (hub) => {
353
+ if (!hub?.platform) return false;
354
+ if (typeof hub.ledger === 'string') return hub.ledger === 'verified';
355
+ return hub.bridge_enabled === true || hub.bridge === true;
356
+ };
357
+
358
+ // ---- the two dials (E28) ------------------------------------------------------------------------
359
+ // Every step declares who does the work and who moves it forward. Shape 4 renames both, and the old
360
+ // names stay readable for a major (rule 3):
361
+ //
362
+ // assistance: none | review | heavy -> driver: human | pair | agent
363
+ // automation: human_approve | machine_advance -> advance: human | auto
364
+ //
365
+ // Same rule as `.sdlc/hub.json` above: BOTH are written, and the OLD one wins while it exists.
366
+ // The old name is what the 29 skills that hand-write `state.json` still emit, and what an older CLI
367
+ // still reads. A step whose dials disagree is a project mid-upgrade, not a decision — `yad doctor`
368
+ // reports it and `yad migrate` closes it.
369
+ export const DRIVER_FROM_ASSISTANCE = { none: 'human', review: 'pair', heavy: 'agent' };
370
+ export const ADVANCE_FROM_AUTOMATION = { human_approve: 'human', machine_advance: 'auto' };
371
+ // Back the other way, for the one place that still has to SAY the old word: `yad next --json` keeps
372
+ // emitting `automation`, because cli/test-golden.mjs deep-equals that output against a frozen v3
373
+ // snapshot and rule 6 says a frozen project's answers never change. The output key follows in the
374
+ // major that removes the old field.
375
+ export const AUTOMATION_FROM_ADVANCE = { human: 'human_approve', auto: 'machine_advance' };
376
+
377
+ // The dial a step is actually running under, from whichever spelling it carries. Old wins.
378
+ // `null` when the step declares nothing, so a caller can tell "not set" from "set to human".
379
+ export const stepAdvance = (step) => {
380
+ if (!step || typeof step !== 'object') return null;
381
+ if (typeof step.automation === 'string') return ADVANCE_FROM_AUTOMATION[step.automation] ?? null;
382
+ if (typeof step.advance === 'string') return step.advance;
383
+ return null;
384
+ };
188
385
 
189
386
  // ---- `yad commit` conventions (mirror skills/sdlc/config.yaml `build`) ----
190
387
  // Conventional-commit types (config.yaml commit_subject_style).
@@ -214,16 +411,21 @@ export const epicFiles = (epicRoot) => ({
214
411
  state: `${epicRoot}/.sdlc/state.json`,
215
412
  approvals: `${epicRoot}/.sdlc/approvals.json`,
216
413
  comments: `${epicRoot}/.sdlc/comments.json`,
414
+ // The record of review PR/MRs opened on the product. `product-prs.json` is the name from shape 3
415
+ // onward; `hub-prs.json` is what it was called before and is written alongside it for one major —
416
+ // both `templates/checks/ledger-guard.sh` and `cli/hook.mjs` name it literally, and the guard in a
417
+ // user's repo only learns the new name when they run `yad update`. See MIRRORED_FILES.
418
+ productPrs: `${epicRoot}/.sdlc/product-prs.json`,
217
419
  hubPrs: `${epicRoot}/.sdlc/hub-prs.json`,
218
420
  contractLock: `${epicRoot}/.sdlc/contract-lock.json`,
219
- // The two append-only back-half ledgers use shard-then-fold storage (cli/ledger.mjs): writers add
421
+ // The two append-only Build ledgers use shard-then-fold storage (cli/ledger.mjs): writers add
220
422
  // one loose shard per entry under the *Dir path (conflict-free concurrent writes); `yad tidy up`
221
423
  // folds finished shards back into the single *Log file. Readers union the folded file + loose shards.
222
424
  buildLog: `${epicRoot}/.sdlc/build-log.json`, // folded ships (also the legacy single file)
223
425
  buildLogDir: `${epicRoot}/.sdlc/build-log`, // one <story>-<task>-<repo>.json per ship
224
426
  trustLog: `${epicRoot}/.sdlc/trust-log.json`, // folded runs (also the legacy single file)
225
427
  trustLogDir: `${epicRoot}/.sdlc/trust-log`, // one <story>-<repo>-<step>-<uid>.json per run
226
- buildStateDir: `${epicRoot}/.sdlc/build-state`, // Phase 4 — per-story, per-repo back-half state
428
+ buildStateDir: `${epicRoot}/.sdlc/build-state`, // Phase 4 — per-story, per-repo Build state
227
429
 
228
430
  change: `${epicRoot}/.sdlc/change.json`, // Phase 6 — change/defect intake + triage
229
431
  reconcileDebt: `${epicRoot}/.sdlc/reconcile-debt.json`, // Phase 6 — hotfix ship-first debt
@@ -235,12 +437,16 @@ export const REPO_WIRING = {
235
437
  common: [
236
438
  { src: 'skills/yad-checks/templates/checks/spec-link.sh', dest: 'checks/spec-link.sh', exec: true },
237
439
  { src: 'skills/yad-checks/templates/checks/contract-check.sh', dest: 'checks/contract-check.sh', exec: true },
440
+ { src: 'skills/yad-checks/templates/checks/package-manager.sh', dest: 'checks/package-manager.sh', exec: true },
441
+ { src: 'skills/yad-checks/templates/checks/install-deps.sh', dest: 'checks/install-deps.sh', exec: true },
238
442
  { src: 'skills/yad-checks/templates/checks/build-test-lint.sh', dest: 'checks/build-test-lint.sh', exec: true },
239
443
  { src: 'skills/yad-checks/templates/checks/lineage-check.sh', dest: 'checks/lineage-check.sh', exec: true },
240
444
  { src: 'skills/yad-checks/templates/checks/epic-open.sh', dest: 'checks/epic-open.sh', exec: true },
241
445
  { src: 'skills/yad-checks/templates/checks/reconcile-debt-check.sh', dest: 'checks/reconcile-debt-check.sh', exec: true },
242
446
  { src: 'skills/yad-checks/templates/checks/verified-commits.sh', dest: 'checks/verified-commits.sh', exec: true },
243
447
  { src: 'skills/yad-checks/templates/checks/commit-message.sh', dest: 'checks/commit-message.sh', exec: true },
448
+ // E65. The CHECK is ours and wired; the map it reads (`.sdlc/risk-map`) is the team's and never is.
449
+ { src: 'skills/yad-checks/templates/checks/risk-map-check.sh', dest: 'checks/risk-map-check.sh', exec: true },
244
450
  { src: 'skills/yad-pr-template/templates/checks/risk-route.sh', dest: 'checks/risk-route.sh', exec: true },
245
451
  { src: 'skills/yad-pr-template/templates/checks/pr-title.sh', dest: 'checks/pr-title.sh', exec: true },
246
452
  { src: 'skills/yad-pr-template/templates/checks/pr-template.sh', dest: 'checks/pr-template.sh', exec: true },
@@ -274,16 +480,16 @@ export const wiringFor = (platform) => [
274
480
  ...(REPO_WIRING[platform] || []),
275
481
  ];
276
482
 
277
- // Hub wiring: CI installed on the PRODUCT HUB itself (dest is the project root — the hub IS the
278
- // root). Installed only when hub.json has a platform and the bridge is enabled. Carries the
483
+ // Product wiring: CI installed on the PRODUCT itself (dest is the project root — the Product IS the
484
+ // root). Installed only when hub.json has a platform and the ledger is verified. Carries the
279
485
  // event-driven gate sync (approvals/change requests/the merge trigger `yad gate ci`) and the
280
- // verified-commits gate (no unverified commits from unverified users reach merge on the hub).
281
- export const HUB_WIRING = {
486
+ // verified-commits gate (no commit without a platform-Verified signature reaches merge on the Product).
487
+ export const PRODUCT_WIRING = {
282
488
  common: [
283
489
  { src: 'skills/yad-checks/templates/checks/verified-commits.sh', dest: 'checks/verified-commits.sh', exec: true },
284
- // The ledger is CI-owned: block non-bot commits to gate-state files on hub review PRs.
490
+ // The ledger is CI-owned: block non-bot commits to gate-state files on the Product review PRs.
285
491
  { src: 'skills/yad-checks/templates/checks/ledger-guard.sh', dest: 'checks/ledger-guard.sh', exec: true },
286
- // Pattern gates run on the hub too (profile: hub) — commit subject + PR title + PR body.
492
+ // Pattern gates run on the Product too (profile: hub) — commit subject + PR title + PR body.
287
493
  { src: 'skills/yad-checks/templates/checks/commit-message.sh', dest: 'checks/commit-message.sh', exec: true },
288
494
  { src: 'skills/yad-pr-template/templates/checks/pr-title.sh', dest: 'checks/pr-title.sh', exec: true },
289
495
  { src: 'skills/yad-pr-template/templates/checks/pr-template.sh', dest: 'checks/pr-template.sh', exec: true },
@@ -292,7 +498,7 @@ export const HUB_WIRING = {
292
498
  { src: 'skills/yad-hub-bridge/templates/github/yad-gate-sync.yml', dest: '.github/workflows/yad-gate-sync.yml' },
293
499
  { src: 'skills/yad-checks/templates/github/yad-verified-commits.yml', dest: '.github/workflows/yad-verified-commits.yml' },
294
500
  { src: 'skills/yad-checks/templates/github/yad-hub-checks.yml', dest: '.github/workflows/yad-hub-checks.yml' },
295
- // Integrity gate for the hub's own direct-to-default pushes (`yad update --push`; the machine-
501
+ // Integrity gate for the Product's own direct-to-default pushes (`yad update --push`; the machine-
296
502
  // state `yad checkpoint`/`gate ci` commits carry [skip ci] and are intentionally not re-checked).
297
503
  { src: 'skills/yad-checks/templates/github/yad-update-guard.yml', dest: '.github/workflows/yad-update-guard.yml' },
298
504
  ],
@@ -304,30 +510,135 @@ export const HUB_WIRING = {
304
510
  ],
305
511
  };
306
512
 
307
- // Harness hooks: the LOCAL half of the ledger rule, installed on the hub beside the CI gates and
308
- // active under the same bridge predicate (#171). Kept out of `HUB_WIRING` because a hook is not a
513
+ // Harness hooks: the LOCAL half of the ledger rule, installed on the Product beside the CI gates and
514
+ // active under the same verified-ledger predicate (#171). Kept out of `PRODUCT_WIRING` because a hook is not a
309
515
  // CI gate — it is advisory, fails open, and its adapter (below) is per-harness, not per-platform.
310
516
  export const HOOK_WIRING = [
311
517
  { src: 'skills/yad-checks/templates/hooks/ledger-guard.sh', dest: 'hooks/ledger-guard.sh', exec: true },
312
518
  ];
313
519
 
314
- // Per-harness adapter config: which IDE target gets a hook entry written, and where.
315
- // `.claude` alone — it is the only supported target with a defined hook protocol (`.agents`,
316
- // `.zencoder` and `.opencode` carry skills only). The others simply get the script and no wiring;
317
- // the contract in the script header is what they would wire by hand.
318
- export const HOOK_SETTINGS = { '.claude': '.claude/settings.json' };
319
- // The tools that can write a file. A `Bash` call (`sed -i epics/…`) is deliberately NOT matched:
320
- // matching it would mean parsing shell, and CI's ledger-guard already fails closed on the result.
321
- export const HOOK_TOOL_MATCHER = 'Edit|Write|MultiEdit|NotebookEdit';
322
- // `$CLAUDE_PROJECT_DIR` so the entry works whatever the harness's working directory is — QUOTED,
323
- // because the harness runs this through a shell: unquoted, a project path containing a space
324
- // word-splits, the command is not found, and the guard is silently off while `check` and `doctor`
325
- // both still report it wired.
326
- export const HOOK_COMMAND = '"$CLAUDE_PROJECT_DIR/hooks/ledger-guard.sh"';
327
- // Spellings a previous yadflow wrote for the SAME hook. An installed entry matching one of these is
328
- // ours to normalise; anything else is the team's, even if it names a similar path. Never widen this
329
- // to a substring test — a team keeping its own wrapper at `.claude/hooks/ledger-guard.sh` would have
330
- // their hook silently rewritten to ours.
331
- export const HOOK_COMMAND_LEGACY = Object.freeze([
332
- '$CLAUDE_PROJECT_DIR/hooks/ledger-guard.sh', // 3.16.x, pre-quoting
333
- ]);
520
+ // Per-harness adapter config: which IDE target gets a hook entry written, where, and in what shape.
521
+ //
522
+ // A harness qualifies for an entry here ONLY if it can run a command BEFORE a file is written and
523
+ // refuse it. That is the whole point of the guard: saying "that edit was wrong" after the write has
524
+ // landed is what CI already does. Two harnesses are wired, both read from their own docs on
525
+ // 2026-09-16. The other install targets were NOT examined for a hook protocol — the scope closed at
526
+ // Cursor — so "no entry here" means "not wired", never "checked and found wanting":
527
+ //
528
+ // .claude Claude Code — `PreToolUse` in `.claude/settings.json`, reading the EXIT CODE, so the
529
+ // entry points straight at the shared `hooks/ledger-guard.sh`.
530
+ // .cursor Cursor — `preToolUse` in `.cursor/hooks.json`, a PERMISSION hook that reads a JSON
531
+ // verdict on STDOUT and treats an empty answer as a refusal. The shared script prints
532
+ // nothing when it allows, so this entry points at `hooks/ledger-guard-cursor.sh`
533
+ // instead (the `wiring` below), which speaks that protocol.
534
+ //
535
+ // Cursor's `afterFileEdit` is deliberately NOT used: it fires once the edit is already on disk, so it
536
+ // could report but never refuse, and a guard that reports is the CI gate we already have.
537
+ //
538
+ // `.agents`, `.gemini`, `.zencoder` and `.opencode` carry skills only. They get the script and no
539
+ // wiring; the contract in the script header is what they would wire by hand, and `yad doctor` NAMES
540
+ // them rather than staying silent — an unguarded target that nothing mentions reads as a guarded one.
541
+ //
542
+ // THE TWO FILE SHAPES, which is what `nested` selects:
543
+ // Claude { hooks: { PreToolUse: [ { matcher, hooks: [ { type: 'command', command } ] } ] } }
544
+ // Cursor { version: 1, hooks: { preToolUse: [ { matcher, command, failClosed } ] } }
545
+ //
546
+ // `preamble` is the keys the FILE itself requires beside `hooks` — Cursor's `version`, which its
547
+ // loader rejects the file without. Written only when the key is ABSENT: a team that pinned a
548
+ // different version has made a choice, and this owns one entry, not their file.
549
+ // The harness's own name for the project root. Two things use it, and only one of them is the wiring:
550
+ // `yad hook ledger-guard` reads it to anchor a RELATIVE path out of the tool-call payload (see
551
+ // `baseDirFor`), and Claude Code's entry also builds its command from it. Cursor's entry does not —
552
+ // see the note on that command — but the variable is still what its guard reads, so it belongs here.
553
+ // Anchoring a payload path against the wrong root walks up to no Product and fails open, which is a
554
+ // guard that is off while every report says it is on.
555
+ const CLAUDE_PROJECT_DIR_ENV = 'CLAUDE_PROJECT_DIR';
556
+ const CURSOR_PROJECT_DIR_ENV = 'CURSOR_PROJECT_DIR';
557
+ const guardCommand = (envVar) => `"$${envVar}/hooks/ledger-guard.sh"`;
558
+
559
+ export const HOOK_ADAPTERS = Object.freeze({
560
+ '.claude': Object.freeze({
561
+ target: '.claude',
562
+ settings: '.claude/settings.json',
563
+ event: 'PreToolUse',
564
+ nested: true,
565
+ preamble: null,
566
+ // The tools that can write a file. A `Bash` call (`sed -i epics/…`) is deliberately NOT matched:
567
+ // matching it would mean parsing shell, and CI's ledger-guard already fails closed on the result.
568
+ matcher: 'Edit|Write|MultiEdit|NotebookEdit',
569
+ // `$CLAUDE_PROJECT_DIR` so the entry works whatever the harness's working directory is — QUOTED,
570
+ // because the harness runs this through a shell: unquoted, a project path containing a space
571
+ // word-splits, the command is not found, and the guard is silently off while `check` and `doctor`
572
+ // both still report it wired. (Cursor's command below is spelled the opposite way, and the note
573
+ // there says why — the two harnesses document different guarantees, so one spelling cannot serve
574
+ // both.)
575
+ projectDirEnv: CLAUDE_PROJECT_DIR_ENV,
576
+ command: guardCommand(CLAUDE_PROJECT_DIR_ENV),
577
+ // Claude Code reads the exit code, so the shared script needs no wrapper here.
578
+ wiring: Object.freeze([]),
579
+ // Spellings a previous yadflow wrote for the SAME hook. An installed entry matching one of these
580
+ // is ours to normalise; anything else is the team's, even if it names a similar path. Never widen
581
+ // this to a substring test — a team keeping its own wrapper at `.claude/hooks/ledger-guard.sh`
582
+ // would have their hook silently rewritten to ours.
583
+ legacyCommands: Object.freeze(['$CLAUDE_PROJECT_DIR/hooks/ledger-guard.sh']), // 3.16.x, pre-quoting
584
+ }),
585
+ '.cursor': Object.freeze({
586
+ target: '.cursor',
587
+ settings: '.cursor/hooks.json',
588
+ event: 'preToolUse',
589
+ nested: false,
590
+ preamble: Object.freeze({ version: 1 }),
591
+ // Cursor's file-writing tool names, which are NOT Claude's — copying `Edit|Write|MultiEdit` across
592
+ // would leave `Delete` unmatched and `MultiEdit` matching a tool Cursor does not have. `Shell` is
593
+ // left out for the same reason `Bash` is above. `Write` and `Delete` are named in Cursor's docs;
594
+ // `Edit` is not, because that list is written as "values include" and does not enumerate the edit
595
+ // tools. It is kept anyway: a name too many costs one regex alternative that never matches, and a
596
+ // name too few is a write nothing intercepts.
597
+ matcher: 'Write|Edit|Delete',
598
+ // Cursor's `preToolUse` is a PERMISSION hook: it answers in JSON on stdout, and empty stdout is
599
+ // invalid JSON, which BLOCKS. So the plain guard cannot be wired here — it prints nothing when it
600
+ // allows, which would have blocked every file write in a verified project. This wrapper, installed
601
+ // with the adapter below, always prints a permission answer. It takes no arguments on purpose:
602
+ // Cursor holds one command STRING and does not document whether it is split by a shell.
603
+ wiring: Object.freeze([
604
+ Object.freeze({ src: 'skills/yad-checks/templates/hooks/ledger-guard-cursor.sh', dest: 'hooks/ledger-guard-cursor.sh', exec: true }),
605
+ ]),
606
+ // RELATIVE, and deliberately not `$CURSOR_PROJECT_DIR/…` — the opposite of the Claude entry above,
607
+ // for a documented reason. Cursor's docs say a project hook RUNS FROM THE PROJECT ROOT, which is
608
+ // the directory holding the `.cursor/hooks.json` this entry lives in, and the same directory
609
+ // holding `hooks/ledger-guard.sh`. So the relative path always resolves, whatever the harness's
610
+ // notion of cwd elsewhere.
611
+ //
612
+ // What they do NOT say is whether the command runs through a shell. That is exactly why there is
613
+ // no variable and no quotes here: `$CURSOR_PROJECT_DIR` never expanded would be a command not
614
+ // found, and quotes taken literally by a non-shell exec would be part of the filename. An
615
+ // unquoted relative path with no variable and no space is the one spelling that works either way
616
+ // — which matters, because every one of those failures is silent, fails OPEN, and leaves
617
+ // `yad doctor` truthfully reporting the entry as wired while nothing is ever refused.
618
+ projectDirEnv: CURSOR_PROJECT_DIR_ENV,
619
+ command: 'hooks/ledger-guard-cursor.sh',
620
+ // This harness answers with a JSON verdict and treats an empty or off-schema answer as a REFUSAL.
621
+ // `yad doctor` uses the flag to catch a hand-wired command that would fail closed — see
622
+ // `miswiredGuardCommand`. An adapter without it reads the exit code, where a silent command is
623
+ // simply a guard that never fires.
624
+ requiresJsonVerdict: true,
625
+ // Nothing shipped before this release, so there is no past spelling of ours to normalise.
626
+ legacyCommands: Object.freeze([]),
627
+ }),
628
+ });
629
+
630
+ // The Claude adapter's fields under their original names. Kept because they are what the shipped
631
+ // error text and the existing tests name, and renaming a constant buys nothing; new code should read
632
+ // the adapter, since these three describe ONE harness and the engine now supports two.
633
+ //
634
+ // `HOOK_SETTINGS` used to live here and is deliberately GONE, not re-derived. It meant "the one
635
+ // wired target and its file", and the obvious re-derivation would have silently changed it to "every
636
+ // adapter's file" — a constant whose meaning moved under callers that never read it again. Nothing
637
+ // imported it; anything that needs a settings path reads it off the adapter.
638
+ export const CLAUDE_HOOK_ADAPTER = HOOK_ADAPTERS['.claude'];
639
+ // Every harness variable that names the project root, for `baseDirFor` in `hook.mjs` to try in turn.
640
+ // Derived from the adapters, so adding a harness up there arms the guard's path resolution too.
641
+ export const HOOK_PROJECT_DIR_ENVS = Object.freeze(Object.values(HOOK_ADAPTERS).map((a) => a.projectDirEnv));
642
+ export const HOOK_TOOL_MATCHER = CLAUDE_HOOK_ADAPTER.matcher;
643
+ export const HOOK_COMMAND = CLAUDE_HOOK_ADAPTER.command;
644
+ export const HOOK_COMMAND_LEGACY = CLAUDE_HOOK_ADAPTER.legacyCommands;