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.
- package/CHANGELOG.md +355 -0
- package/README.md +79 -26
- package/bin/commands.mjs +41 -0
- package/bin/yad.mjs +437 -124
- package/cli/artifact-status.mjs +34 -15
- package/cli/checkpoint.mjs +69 -49
- package/cli/codeowners-command.mjs +170 -0
- package/cli/codeowners.mjs +397 -0
- package/cli/commit.mjs +13 -9
- package/cli/companion.mjs +2 -2
- package/cli/dial.mjs +183 -0
- package/cli/docs.mjs +88 -32
- package/cli/doctor.mjs +1472 -97
- package/cli/epic-state.mjs +3478 -232
- package/cli/epic.mjs +506 -0
- package/cli/errors.mjs +4 -1
- package/cli/gate.mjs +1002 -209
- package/cli/history.mjs +556 -0
- package/cli/hook.mjs +266 -55
- package/cli/hubcommit.mjs +6 -17
- package/cli/index-command.mjs +87 -0
- package/cli/ledger.mjs +57 -7
- package/cli/lib.mjs +184 -18
- package/cli/manifest.mjs +367 -56
- package/cli/migrate.mjs +726 -53
- package/cli/mode.mjs +170 -0
- package/cli/next.mjs +349 -90
- package/cli/openpr.mjs +191 -39
- package/cli/people.mjs +654 -0
- package/cli/plan.mjs +417 -132
- package/cli/platform.mjs +110 -129
- package/cli/product-index.mjs +287 -0
- package/cli/protection.mjs +706 -0
- package/cli/reconcile.mjs +38 -12
- package/cli/repo-publish.mjs +24 -26
- package/cli/repo.mjs +23 -14
- package/cli/report.mjs +21 -15
- package/cli/review.mjs +24 -27
- package/cli/riskmap-command.mjs +289 -0
- package/cli/riskmap.mjs +373 -0
- package/cli/setup.mjs +139 -287
- package/cli/ship.mjs +7 -6
- package/cli/skill.mjs +180 -0
- package/cli/skip.mjs +211 -30
- package/cli/thread.mjs +42 -17
- package/cli/tidy.mjs +20 -20
- package/cli/update-commit.mjs +22 -22
- package/cli/usage.mjs +115 -109
- package/package.json +3 -3
- package/skills/sdlc/config.yaml +166 -87
- package/skills/sdlc/module-help.csv +35 -35
- package/skills/yad-analysis/SKILL.md +125 -65
- package/skills/yad-architecture/SKILL.md +34 -23
- package/skills/yad-architecture/references/contract-format.md +10 -8
- package/skills/yad-backfill/SKILL.md +14 -8
- package/skills/yad-backfill/references/backfill.md +1 -1
- package/skills/yad-change/SKILL.md +127 -52
- package/skills/yad-change/references/triage.md +42 -28
- package/skills/yad-checks/SKILL.md +89 -45
- package/skills/yad-checks/references/check-gates.md +315 -92
- package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
- package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
- package/skills/yad-checks/templates/checks/contract-check.sh +58 -2
- package/skills/yad-checks/templates/checks/epic-open.sh +3 -3
- package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
- package/skills/yad-checks/templates/checks/ledger-guard.sh +94 -18
- package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
- package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +4 -4
- package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
- package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
- package/skills/yad-checks/templates/github/yad-checks.yml +37 -5
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
- package/skills/yad-checks/templates/github/yad-update-guard.yml +3 -4
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
- package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +22 -4
- package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +4 -4
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +38 -7
- package/skills/yad-commit/SKILL.md +6 -6
- package/skills/yad-connect-design/SKILL.md +6 -6
- package/skills/yad-connect-design/references/design-context.md +1 -1
- package/skills/yad-connect-design/references/design-registry.md +2 -2
- package/skills/yad-connect-docs/SKILL.md +12 -12
- package/skills/yad-connect-docs/references/docs-registry.md +1 -1
- package/skills/yad-connect-learning/SKILL.md +5 -5
- package/skills/yad-connect-learning/references/learning-registry.md +2 -2
- package/skills/yad-connect-repos/SKILL.md +92 -54
- package/skills/yad-connect-repos/references/code-context.md +6 -6
- package/skills/yad-connect-repos/references/hub-config.md +68 -58
- package/skills/yad-connect-repos/references/repos-registry.md +10 -9
- package/skills/yad-connect-repos/references/risk-map.md +81 -0
- package/skills/yad-connect-testing/SKILL.md +6 -6
- package/skills/yad-connect-testing/references/testing-context.md +3 -4
- package/skills/yad-connect-testing/references/testing-registry.md +2 -2
- package/skills/yad-defects/SKILL.md +8 -8
- package/skills/yad-discovery/SKILL.md +130 -94
- package/skills/yad-discovery/references/discovery-schema.md +23 -7
- package/skills/yad-discovery/references/foundation-schema.md +374 -0
- package/skills/yad-docs/SKILL.md +16 -11
- package/skills/yad-docs/references/data-mapping.md +9 -7
- package/skills/yad-docs/templates/app/package-lock.json +3 -3
- package/skills/yad-docs-overview/SKILL.md +32 -17
- package/skills/yad-docs-overview/references/pipeline-model.md +47 -28
- package/skills/yad-docs-sync/SKILL.md +10 -5
- package/skills/yad-docs-sync/references/staleness.md +8 -7
- package/skills/yad-engineer-review/SKILL.md +88 -24
- package/skills/yad-engineer-review/references/ship-and-record.md +25 -16
- package/skills/yad-epic/SKILL.md +178 -100
- package/skills/yad-epic/references/state-schema.md +626 -117
- package/skills/yad-hub-bridge/SKILL.md +66 -48
- package/skills/yad-hub-bridge/references/bridge.md +110 -83
- package/skills/yad-hub-bridge/references/login-roster.md +163 -70
- package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
- package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
- package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
- package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +22 -12
- package/skills/yad-implement/SKILL.md +29 -15
- package/skills/yad-implement/references/implement-conventions.md +2 -2
- package/skills/yad-learn/SKILL.md +9 -9
- package/skills/yad-learn/references/learning-state.md +2 -2
- package/skills/yad-open-pr/SKILL.md +64 -29
- package/skills/yad-pair-review/SKILL.md +18 -16
- package/skills/yad-pair-review/references/session-state.md +4 -4
- package/skills/yad-pr-template/SKILL.md +48 -27
- package/skills/yad-pr-template/references/risk-routing.md +97 -24
- package/skills/yad-pr-template/templates/checks/pr-template.sh +37 -15
- package/skills/yad-pr-template/templates/checks/pr-title.sh +27 -13
- package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
- package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
- package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
- package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +15 -14
- package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +15 -13
- package/skills/yad-reconcile/SKILL.md +3 -3
- package/skills/yad-report/SKILL.md +5 -5
- package/skills/yad-review-companion/SKILL.md +12 -9
- package/skills/yad-review-gate/SKILL.md +198 -79
- package/skills/yad-review-gate/references/gating.md +230 -54
- package/skills/yad-run/SKILL.md +86 -56
- package/skills/yad-run/references/run-loop.md +67 -45
- package/skills/yad-ship/SKILL.md +18 -14
- package/skills/yad-spec/SKILL.md +31 -17
- package/skills/yad-spec/references/spec-handoff.md +17 -5
- package/skills/yad-status/SKILL.md +114 -56
- package/skills/yad-stories/SKILL.md +42 -27
- package/skills/yad-stories/references/story-schema.md +10 -9
- package/skills/yad-stub/SKILL.md +59 -48
- package/skills/yad-sync-repos/SKILL.md +3 -3
- package/skills/yad-test-cases/SKILL.md +37 -30
- package/skills/yad-test-cases/references/test-cases-schema.md +8 -5
- package/skills/yad-timeline/SKILL.md +8 -7
- package/skills/yad-ui/SKILL.md +46 -25
- package/cli/roster.mjs +0 -164
- 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
|
-
|
|
4
|
-
import
|
|
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
|
|
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
|
|
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
|
-
//
|
|
125
|
-
|
|
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
|
|
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":
|
|
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
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
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
|
-
//
|
|
184
|
-
// one
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
export const
|
|
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
|
|
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
|
|
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
|
-
//
|
|
278
|
-
// root). Installed only when hub.json has a platform and 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
|
|
281
|
-
export const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
308
|
-
// active under the same
|
|
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
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
// the
|
|
318
|
-
|
|
319
|
-
// The
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
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;
|