yadflow 4.0.0-next.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +699 -0
- package/README.md +85 -22
- package/bin/commands.mjs +3 -1
- package/bin/yad.mjs +183 -29
- package/cli/capture.mjs +4 -2
- package/cli/checkpoint.mjs +10 -10
- package/cli/claims.mjs +4 -4
- package/cli/commit.mjs +48 -12
- package/cli/docs.mjs +15 -13
- package/cli/doctor.mjs +557 -141
- package/cli/epic-state.mjs +41 -18
- package/cli/errors.mjs +3 -2
- package/cli/find-product.mjs +197 -0
- package/cli/fold.mjs +4 -4
- package/cli/gate-local.mjs +13 -13
- package/cli/gate.mjs +111 -103
- package/cli/history.mjs +13 -13
- package/cli/hook.mjs +201 -30
- package/cli/index-command.mjs +6 -6
- package/cli/lib.mjs +91 -29
- package/cli/manifest.mjs +171 -104
- package/cli/migrate.mjs +208 -17
- package/cli/mode.mjs +22 -22
- package/cli/next.mjs +6 -6
- package/cli/openpr.mjs +32 -24
- package/cli/people.mjs +22 -10
- package/cli/plan.mjs +718 -45
- package/cli/platform.mjs +13 -7
- package/cli/product-index.mjs +4 -4
- package/cli/{hubcommit.mjs → productcommit.mjs} +11 -7
- package/cli/protection.mjs +19 -9
- package/cli/reconcile.mjs +98 -23
- package/cli/repo-publish.mjs +22 -16
- package/cli/repo.mjs +128 -33
- package/cli/report.mjs +4 -2
- package/cli/review.mjs +26 -19
- package/cli/setup.mjs +105 -36
- package/cli/ship.mjs +1 -1
- package/cli/skip.mjs +4 -4
- package/cli/tidy.mjs +7 -7
- package/cli/update-commit.mjs +22 -19
- package/cli/workspace.mjs +495 -0
- package/package.json +7 -4
- package/skills/sdlc/config.yaml +14 -13
- package/skills/sdlc/module-help.csv +8 -8
- package/skills/yad-analysis/SKILL.md +6 -4
- package/skills/yad-architecture/SKILL.md +4 -3
- package/skills/yad-backfill/SKILL.md +11 -5
- package/skills/yad-backfill/references/backfill.md +33 -3
- package/skills/yad-backfill/templates/checks/backfill-check.sh +200 -17
- package/skills/yad-change/SKILL.md +1 -1
- package/skills/yad-checks/SKILL.md +48 -27
- package/skills/yad-checks/references/check-gates.md +213 -53
- package/skills/yad-checks/templates/checks/commit-message.sh +39 -13
- package/skills/yad-checks/templates/checks/contract-check.sh +472 -23
- package/skills/yad-checks/templates/checks/epic-open.sh +398 -8
- package/skills/yad-checks/templates/checks/ledger-guard.sh +75 -25
- package/skills/yad-checks/templates/checks/lineage-check.sh +391 -7
- package/skills/yad-checks/templates/checks/product-checkout.sh +141 -0
- package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +399 -8
- package/skills/yad-checks/templates/checks/risk-map-check.sh +35 -3
- package/skills/yad-checks/templates/checks/spec-link.sh +36 -3
- package/skills/yad-checks/templates/checks/verified-commits.sh +26 -2
- package/skills/yad-checks/templates/github/yad-checks.yml +33 -13
- package/skills/yad-checks/templates/github/{yad-hub-checks.yml → yad-product-checks.yml} +14 -10
- package/skills/yad-checks/templates/github/yad-update-guard.yml +1 -1
- package/skills/yad-checks/templates/github/yad-verified-commits.yml +1 -1
- package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +12 -0
- package/skills/yad-checks/templates/gitlab/{yad-hub-checks.gitlab-ci.yml → yad-product-checks.gitlab-ci.yml} +16 -16
- package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +2 -2
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.mjs +130 -0
- package/skills/yad-checks/templates/hooks/ledger-guard.mjs +117 -0
- package/skills/yad-checks/templates/hooks/yad-capture.mjs +117 -0
- package/skills/yad-commit/SKILL.md +18 -7
- package/skills/yad-connect-design/SKILL.md +1 -1
- package/skills/yad-connect-design/references/design-registry.md +1 -1
- package/skills/yad-connect-docs/SKILL.md +16 -11
- package/skills/yad-connect-docs/references/docs-registry.md +8 -8
- package/skills/yad-connect-learning/SKILL.md +1 -1
- package/skills/yad-connect-learning/references/learning-registry.md +1 -1
- package/skills/yad-connect-repos/SKILL.md +31 -8
- package/skills/yad-connect-repos/references/code-context.md +1 -1
- package/skills/yad-connect-repos/references/{hub-config.md → product-config.md} +19 -11
- package/skills/yad-connect-repos/references/repos-registry.md +1 -1
- package/skills/yad-connect-testing/SKILL.md +1 -1
- package/skills/yad-connect-testing/references/testing-registry.md +1 -1
- package/skills/yad-discovery/SKILL.md +2 -2
- package/skills/yad-discovery/references/discovery-schema.md +1 -1
- package/skills/yad-docs/SKILL.md +1 -1
- package/skills/yad-docs/references/data-mapping.md +1 -1
- package/skills/yad-docs/templates/app/package-lock.json +8 -8
- package/skills/yad-docs/templates/app/package.json +1 -1
- package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +2 -2
- package/skills/yad-docs-overview/references/pipeline-model.md +3 -3
- package/skills/yad-engineer-review/SKILL.md +2 -2
- package/skills/yad-engineer-review/references/ship-and-record.md +3 -3
- package/skills/yad-epic/SKILL.md +6 -4
- package/skills/yad-epic/references/state-schema.md +13 -12
- package/skills/yad-open-pr/SKILL.md +1 -1
- package/skills/yad-pair-review/SKILL.md +5 -5
- package/skills/yad-pair-review/references/session-state.md +2 -2
- package/skills/yad-pr-template/SKILL.md +14 -12
- package/skills/yad-pr-template/templates/checks/pr-template.sh +18 -16
- package/skills/yad-pr-template/templates/checks/pr-title.sh +15 -13
- package/skills/yad-pr-template/templates/{hub → product}/github/pull_request_template.md +2 -2
- package/skills/yad-pr-template/templates/{hub → product}/gitlab/merge_request_templates/Default.md +2 -2
- package/skills/{yad-hub-bridge → yad-product-bridge}/SKILL.md +15 -15
- package/skills/{yad-hub-bridge → yad-product-bridge}/references/bridge.md +15 -10
- package/skills/{yad-hub-bridge → yad-product-bridge}/references/login-roster.md +2 -2
- package/skills/{yad-hub-bridge/templates/checks/hub-route.sh → yad-product-bridge/templates/checks/product-route.sh} +2 -2
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/github/yad-gate-sync.yml +29 -16
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/gitlab-ci.include-root.yml +1 -1
- package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/yad-gate-sync.gitlab-ci.yml +5 -3
- package/skills/yad-report/SKILL.md +1 -1
- package/skills/yad-review-companion/SKILL.md +3 -3
- package/skills/yad-review-gate/SKILL.md +22 -23
- package/skills/yad-review-gate/references/gating.md +13 -13
- package/skills/yad-run/SKILL.md +2 -2
- package/skills/yad-run/references/run-loop.md +2 -2
- package/skills/yad-spec/SKILL.md +2 -1
- package/skills/yad-spec/references/spec-handoff.md +19 -10
- package/skills/yad-stories/SKILL.md +2 -2
- package/skills/yad-sync-repos/SKILL.md +19 -3
- package/skills/yad-test-cases/SKILL.md +2 -2
- package/skills/yad-ui/SKILL.md +2 -2
- package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +0 -91
- package/skills/yad-checks/templates/hooks/ledger-guard.sh +0 -100
- package/skills/yad-checks/templates/hooks/yad-capture.sh +0 -53
package/cli/manifest.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export const SKILLS = [
|
|
|
44
44
|
'yad-implement',
|
|
45
45
|
'yad-checks',
|
|
46
46
|
'yad-pr-template',
|
|
47
|
-
'yad-
|
|
47
|
+
'yad-product-bridge',
|
|
48
48
|
'yad-commit',
|
|
49
49
|
'yad-open-pr',
|
|
50
50
|
'yad-ship',
|
|
@@ -63,8 +63,11 @@ export const SKILLS = [
|
|
|
63
63
|
'yad-stub',
|
|
64
64
|
];
|
|
65
65
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
66
|
+
// Old skill names. `check`/`update` migrate any install still carrying one: remove the old copy,
|
|
67
|
+
// install the renamed one. A value is one old name or a list of them — a skill renamed twice keeps
|
|
68
|
+
// every name it shipped under, since an install can skip releases. Read through `legacySkillPairs`.
|
|
69
|
+
// sdlc-* -> yad-* the 2.0 rename.
|
|
70
|
+
// yad-hub-bridge the 4.0 hub -> Product rename (E123).
|
|
68
71
|
export const LEGACY_SKILLS = {
|
|
69
72
|
'yad-analysis': 'sdlc-author-analysis',
|
|
70
73
|
'yad-epic': 'sdlc-author-epic',
|
|
@@ -76,7 +79,7 @@ export const LEGACY_SKILLS = {
|
|
|
76
79
|
'yad-implement': 'sdlc-implement',
|
|
77
80
|
'yad-checks': 'sdlc-checks',
|
|
78
81
|
'yad-pr-template': 'sdlc-pr-template',
|
|
79
|
-
'yad-
|
|
82
|
+
'yad-product-bridge': ['sdlc-hub-bridge', 'yad-hub-bridge'],
|
|
80
83
|
// Step E ("ship") was renamed to yad-engineer-review (the yad-ship name now belongs to the new
|
|
81
84
|
// commit+open-PR combined skill). Pre-2.0 installs carry sdlc-ship → migrate it to yad-engineer-review.
|
|
82
85
|
// A 2.x install carrying yad-ship-as-Step-E is simply overwritten with the new combined content on
|
|
@@ -87,6 +90,9 @@ export const LEGACY_SKILLS = {
|
|
|
87
90
|
'yad-review-gate': 'sdlc-review-gate',
|
|
88
91
|
'yad-status': 'sdlc-status',
|
|
89
92
|
};
|
|
93
|
+
// Every (current name, old name) pair in LEGACY_SKILLS, one pair per old name.
|
|
94
|
+
export const legacySkillPairs = () => Object.entries(LEGACY_SKILLS)
|
|
95
|
+
.flatMap(([skill, old]) => (Array.isArray(old) ? old : [old]).map((o) => [skill, o]));
|
|
90
96
|
|
|
91
97
|
// Skills removed in a later release that must be PURGED from existing installs. Unlike
|
|
92
98
|
// LEGACY_SKILLS (a rename: drop old name, install new), these have no replacement — a rerun of
|
|
@@ -96,22 +102,28 @@ export const LEGACY_SKILLS = {
|
|
|
96
102
|
// yad-review-comments — removed in 2.x (was sdlc-review-comments pre-2.0).
|
|
97
103
|
export const REMOVED_SKILLS = ['yad-review-comments', 'sdlc-review-comments'];
|
|
98
104
|
|
|
99
|
-
//
|
|
100
|
-
// An old file is removed ONLY when its first line
|
|
101
|
-
//
|
|
105
|
+
// Wired-file dests replaced by renamed ones (old dest -> { to: new dest, marker }, per platform).
|
|
106
|
+
// An old file is removed ONLY when its first line starts with that entry's ownership marker — a
|
|
107
|
+
// same-named file the user authored themselves is never touched. Each entry carries its own marker
|
|
108
|
+
// because each rename happened under a different one: the pre-2.0 files start `# sdlc-managed`, the
|
|
109
|
+
// hub -> Product ones (E123) `# yad-managed: yad-checks` (GitHub) or `# yad-managed-include: yad-checks`
|
|
110
|
+
// (GitLab). A marker is not proof nobody edited the file: `legacyFileActions` (cli/plan.mjs) also
|
|
111
|
+
// asks the provenance record, as every other managed file does.
|
|
102
112
|
export const LEGACY_MARKER = '# sdlc-managed';
|
|
103
113
|
export const LEGACY_REPO_FILES = {
|
|
104
|
-
github: { '.github/workflows/sdlc-checks.yml': '.github/workflows/yad-checks.yml' },
|
|
105
|
-
gitlab: { '.gitlab/ci/sdlc-checks.yml': '.gitlab/ci/yad-checks.yml' },
|
|
114
|
+
github: { '.github/workflows/sdlc-checks.yml': { to: '.github/workflows/yad-checks.yml', marker: LEGACY_MARKER } },
|
|
115
|
+
gitlab: { '.gitlab/ci/sdlc-checks.yml': { to: '.gitlab/ci/yad-checks.yml', marker: LEGACY_MARKER } },
|
|
106
116
|
};
|
|
107
117
|
export const LEGACY_PRODUCT_FILES = {
|
|
108
118
|
github: {
|
|
109
|
-
'.github/workflows/sdlc-gate-sync.yml': '.github/workflows/yad-gate-sync.yml',
|
|
110
|
-
'.github/workflows/sdlc-verified-commits.yml': '.github/workflows/yad-verified-commits.yml',
|
|
119
|
+
'.github/workflows/sdlc-gate-sync.yml': { to: '.github/workflows/yad-gate-sync.yml', marker: LEGACY_MARKER },
|
|
120
|
+
'.github/workflows/sdlc-verified-commits.yml': { to: '.github/workflows/yad-verified-commits.yml', marker: LEGACY_MARKER },
|
|
121
|
+
'.github/workflows/yad-hub-checks.yml': { to: '.github/workflows/yad-product-checks.yml', marker: '# yad-managed: yad-checks' },
|
|
111
122
|
},
|
|
112
123
|
gitlab: {
|
|
113
|
-
'.gitlab/ci/sdlc-gate-sync.yml': '.gitlab/ci/yad-gate-sync.yml',
|
|
114
|
-
'.gitlab/ci/sdlc-verified-commits.yml': '.gitlab/ci/yad-verified-commits.yml',
|
|
124
|
+
'.gitlab/ci/sdlc-gate-sync.yml': { to: '.gitlab/ci/yad-gate-sync.yml', marker: LEGACY_MARKER },
|
|
125
|
+
'.gitlab/ci/sdlc-verified-commits.yml': { to: '.gitlab/ci/yad-verified-commits.yml', marker: LEGACY_MARKER },
|
|
126
|
+
'.gitlab/ci/yad-hub-checks.yml': { to: '.gitlab/ci/yad-product-checks.yml', marker: '# yad-managed-include: yad-checks' },
|
|
115
127
|
},
|
|
116
128
|
};
|
|
117
129
|
|
|
@@ -254,7 +266,7 @@ export const PROJECT_FILES = {
|
|
|
254
266
|
// The product's own settings. `product.json` is the name from shape 3 onward; `hub.json` is what
|
|
255
267
|
// it was called before, and it is NOT dead — see MIRRORED_FILES below.
|
|
256
268
|
productConfig: '.sdlc/product.json',
|
|
257
|
-
|
|
269
|
+
productConfigLegacy: '.sdlc/hub.json',
|
|
258
270
|
designConfig: '.sdlc/design.json',
|
|
259
271
|
testingConfig: '.sdlc/testing.json',
|
|
260
272
|
learningConfig: '.sdlc/learning.json',
|
|
@@ -274,60 +286,74 @@ export const PROJECT_FILES = {
|
|
|
274
286
|
|
|
275
287
|
// ---- files that changed NAME in shape 3 --------------------------------------------------------
|
|
276
288
|
//
|
|
277
|
-
// `hub` became `Product`, so `.sdlc/hub.json` became `.sdlc/product.json
|
|
278
|
-
// a FILE rename is not, because a
|
|
289
|
+
// `hub` became `Product`, so `.sdlc/hub.json` became `.sdlc/product.json` (and each epic's
|
|
290
|
+
// `hub-prs.json` became `product-prs.json`). A field rename is easy; a FILE rename is not, because a
|
|
291
|
+
// file is opened by name from outside this codebase:
|
|
279
292
|
//
|
|
280
|
-
// - `templates/checks/
|
|
281
|
-
// `.sdlc/hub.json` by that literal
|
|
282
|
-
//
|
|
283
|
-
// - So a project WILL exist
|
|
284
|
-
//
|
|
293
|
+
// - the check gates (`templates/checks/*.sh`, `checks/verified-commits.sh`) are committed inside
|
|
294
|
+
// the USER's repo, and the ones installed before E122 open `.sdlc/hub.json` by that literal
|
|
295
|
+
// path. They are refreshed by `yad update`, which is a separate act from `yad migrate`.
|
|
296
|
+
// - So a project WILL exist whose gates are older than its CLI. If only the new name existed, such
|
|
297
|
+
// a `ledger-guard` would read no platform, conclude the ledger is local, and stop rejecting human
|
|
285
298
|
// commits to it — the audit trail disarmed by an upgrade.
|
|
286
299
|
//
|
|
287
|
-
// Hence
|
|
288
|
-
//
|
|
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.
|
|
300
|
+
// Hence BOTH names are written on every save (`writeMirrored`, cli/lib.mjs) until v5, and the gates
|
|
301
|
+
// installed from this release on read the new name first.
|
|
291
302
|
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
303
|
+
// The ladder (E122, 2026-09-28). It was three majors — add, switch, delete — and is now two, because
|
|
304
|
+
// 4.0.0 reached `@next` only and no ordinary user lived with the add-only stage:
|
|
305
|
+
// v3 the new name appeared beside the old one; the OLD one was read
|
|
306
|
+
// v4 (this) the NEW name is read; the old one is still written, for the gates above;
|
|
307
|
+
// `yad doctor` says v5 deletes it
|
|
308
|
+
// v5 the old name is deleted (a shape of its own, with a `MIGRATIONS` step)
|
|
295
309
|
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
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.
|
|
310
|
+
// Two copies that DISAGREE are never settled by picking one. Whichever name won, somebody's edit —
|
|
311
|
+
// a person editing the name they know, a script, an older gate's world view — would be silently
|
|
312
|
+
// ignored. So every command refuses (`mirrorDrift`, checked once in bin/yad.mjs and again in
|
|
313
|
+
// `loadLedger`), names both files, and points at `yad migrate`, which asks which copy wins.
|
|
303
314
|
export const MIRRORED_FILES = [
|
|
304
|
-
{ canonical: PROJECT_FILES.productConfig, legacy: PROJECT_FILES.
|
|
315
|
+
{ canonical: PROJECT_FILES.productConfig, legacy: PROJECT_FILES.productConfigLegacy },
|
|
305
316
|
];
|
|
306
317
|
|
|
307
|
-
// Which of the two names to READ
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
318
|
+
// Which of the two names to READ: the new one when it exists, the old one otherwise, and the new one
|
|
319
|
+
// again when neither does (so "absent" is reported under the name people should use). A project
|
|
320
|
+
// holding either name alone still works. Pure on purpose — it is a probe used by every directory
|
|
321
|
+
// walk that asks "is this a Product?", so it must never throw; the refusal lives in `mirrorDrift`.
|
|
322
|
+
export const preferring = (canonical, legacy) => (existsSync(canonical) || !existsSync(legacy) ? canonical : legacy);
|
|
323
|
+
|
|
324
|
+
export const productConfigPath = (root) =>
|
|
325
|
+
preferring(path.join(root, PROJECT_FILES.productConfig), path.join(root, PROJECT_FILES.productConfigLegacy));
|
|
326
|
+
|
|
327
|
+
// Where a person edits the Product settings, for a hint (E122): the file that is READ, and — only when
|
|
328
|
+
// both names exist — the command that copies the edit over the other one. Naming product.json on a
|
|
329
|
+
// Product that has only hub.json would have a person create a one-key product.json, and `--keep product`
|
|
330
|
+
// would then copy it over every other setting.
|
|
331
|
+
export const settingsEditHint = (root) => {
|
|
332
|
+
const rel = path.relative(root, productConfigPath(root)).split(path.sep).join('/');
|
|
333
|
+
const both = existsSync(path.join(root, PROJECT_FILES.productConfig)) && existsSync(path.join(root, PROJECT_FILES.productConfigLegacy));
|
|
334
|
+
return both ? `${rel}, then \`yad migrate --apply --keep product\`` : rel;
|
|
335
|
+
};
|
|
336
|
+
// The same, where no Product root is at hand.
|
|
337
|
+
export const SETTINGS_EDIT_HINT = '.sdlc/product.json (.sdlc/hub.json on a Product that has only that name), then `yad migrate --apply --keep product` if both exist';
|
|
338
|
+
|
|
339
|
+
// Both names present and saying different things. Byte for byte, like `yad doctor` and the gates
|
|
340
|
+
// (`cmp -s`): the engine writes both from one string, so any difference at all came from outside it.
|
|
341
|
+
// A copy that cannot be read counts as different — it is not the same file.
|
|
342
|
+
export const mirrorDrift = (canonical, legacy) => {
|
|
343
|
+
if (!existsSync(canonical) || !existsSync(legacy)) return false;
|
|
344
|
+
try { return !readFileSync(canonical).equals(readFileSync(legacy)); } catch { return true; }
|
|
327
345
|
};
|
|
328
346
|
|
|
329
|
-
//
|
|
330
|
-
|
|
347
|
+
// The drifted pairs under a Product root, project-relative: the settings file and every epic's PR
|
|
348
|
+
// ledger. Empty when nothing disagrees. `epicDirs` is passed in (manifest.mjs lists no folders).
|
|
349
|
+
export const productDrift = (root, epicDirs = []) => {
|
|
350
|
+
const pairs = [...MIRRORED_FILES];
|
|
351
|
+
for (const e of epicDirs) {
|
|
352
|
+
const f = epicFiles(e);
|
|
353
|
+
pairs.push({ canonical: f.productPrs, legacy: f.productPrsLegacy });
|
|
354
|
+
}
|
|
355
|
+
return pairs.filter(({ canonical, legacy }) => mirrorDrift(path.join(root, canonical), path.join(root, legacy)));
|
|
356
|
+
};
|
|
331
357
|
|
|
332
358
|
// Who writes the ledger. Two values, and the switch lives in `.sdlc/hub.json`:
|
|
333
359
|
//
|
|
@@ -349,10 +375,10 @@ export const preferring = (canonical, legacy) => (existsSync(legacy) ? legacy :
|
|
|
349
375
|
// SAME order in bash because the check gates are standalone by design; that copy is the only one,
|
|
350
376
|
// its header says so, and cli/test-checks.mjs runs a table of hub.json variants through both and
|
|
351
377
|
// asserts they agree on every row. Three keys is three ways for two readers to drift.
|
|
352
|
-
export const isVerifiedLedger = (
|
|
353
|
-
if (!
|
|
354
|
-
if (typeof
|
|
355
|
-
return
|
|
378
|
+
export const isVerifiedLedger = (productConfig) => {
|
|
379
|
+
if (!productConfig?.platform) return false;
|
|
380
|
+
if (typeof productConfig.ledger === 'string') return productConfig.ledger === 'verified';
|
|
381
|
+
return productConfig.bridge_enabled === true || productConfig.bridge === true;
|
|
356
382
|
};
|
|
357
383
|
|
|
358
384
|
// ---- the two dials (E28) ------------------------------------------------------------------------
|
|
@@ -362,7 +388,8 @@ export const isVerifiedLedger = (hub) => {
|
|
|
362
388
|
// assistance: none | review | heavy -> driver: human | pair | agent
|
|
363
389
|
// automation: human_approve | machine_advance -> advance: human | auto
|
|
364
390
|
//
|
|
365
|
-
//
|
|
391
|
+
// BOTH spellings are written, and the OLD one wins while it exists. (Not the rule the renamed files
|
|
392
|
+
// follow since E122 — those read the new name first. The dials keep old-first until their own switch.)
|
|
366
393
|
// The old name is what the 29 skills that hand-write `state.json` still emit, and what an older CLI
|
|
367
394
|
// still reads. A step whose dials disagree is a project mid-upgrade, not a decision — `yad doctor`
|
|
368
395
|
// reports it and `yad migrate` closes it.
|
|
@@ -397,14 +424,19 @@ export const AI_COAUTHORS = {
|
|
|
397
424
|
};
|
|
398
425
|
// Atomic-commit guard: warn/refuse above this many staged files (build plan: ≤3 where possible).
|
|
399
426
|
export const ATOMIC_FILE_LIMIT = 3;
|
|
400
|
-
// Trailer order is fixed: Task -> Contract-Change -> Co-Authored-By (config.yaml build
|
|
427
|
+
// Trailer order is fixed: Task -> Contract-Change -> Ledger-Override -> Co-Authored-By (config.yaml build
|
|
428
|
+
// comment; Ledger-Override is E49's `yad commit --manual --reason`).
|
|
401
429
|
export const TASK_TRAILER = 'Task';
|
|
402
430
|
export const CONTRACT_CHANGE_TRAILER = 'Contract-Change';
|
|
431
|
+
// E49: `yad commit --manual --reason` records why a person committed past the ledger hook. The CI
|
|
432
|
+
// `ledger-guard` check quotes it and still fails the commit — anyone can type a trailer.
|
|
433
|
+
export const LEDGER_OVERRIDE_TRAILER = 'Ledger-Override';
|
|
403
434
|
export const COAUTHOR_TRAILER = 'Co-Authored-By';
|
|
404
|
-
// A Task trailer id must be a real <story>-T<NN
|
|
405
|
-
// (checks/spec-link.sh:
|
|
406
|
-
// rejected locally at commit time instead of after a push +
|
|
407
|
-
|
|
435
|
+
// A Task trailer id must be a real <story>-T<NN>, the story an `EP-<slug>-S<n>` with a lowercase slug.
|
|
436
|
+
// Mirrors the spec-link gate contract (checks/spec-link.sh: `-T[0-9]+$`, then STORY_RE on the story, E117)
|
|
437
|
+
// so an explicit --task that would fail CI is rejected locally at commit time instead of after a push +
|
|
438
|
+
// history rewrite.
|
|
439
|
+
export const TASK_ID_RE = /^EP-[a-z0-9-]+-S\d+-T\d+$/;
|
|
408
440
|
|
|
409
441
|
// Per-epic ledger files under epics/<epic>/.sdlc/ (the file source of truth the gate reads/writes).
|
|
410
442
|
export const epicFiles = (epicRoot) => ({
|
|
@@ -412,11 +444,11 @@ export const epicFiles = (epicRoot) => ({
|
|
|
412
444
|
approvals: `${epicRoot}/.sdlc/approvals.json`,
|
|
413
445
|
comments: `${epicRoot}/.sdlc/comments.json`,
|
|
414
446
|
// 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
|
|
416
|
-
//
|
|
417
|
-
//
|
|
447
|
+
// onward and the one read from v4; `hub-prs.json` is what it was called before and is still written
|
|
448
|
+
// beside it until v5 — `templates/checks/ledger-guard.sh` guards both names, and a guard in a user's
|
|
449
|
+
// repo only learns anything new when they run `yad update`. See MIRRORED_FILES.
|
|
418
450
|
productPrs: `${epicRoot}/.sdlc/product-prs.json`,
|
|
419
|
-
|
|
451
|
+
productPrsLegacy: `${epicRoot}/.sdlc/hub-prs.json`,
|
|
420
452
|
contractLock: `${epicRoot}/.sdlc/contract-lock.json`,
|
|
421
453
|
// The two append-only Build ledgers use shard-then-fold storage (cli/ledger.mjs): writers add
|
|
422
454
|
// one loose shard per entry under the *Dir path (conflict-free concurrent writes); `yad tidy up`
|
|
@@ -443,6 +475,8 @@ export const REPO_WIRING = {
|
|
|
443
475
|
{ src: 'skills/yad-checks/templates/checks/lineage-check.sh', dest: 'checks/lineage-check.sh', exec: true },
|
|
444
476
|
{ src: 'skills/yad-checks/templates/checks/epic-open.sh', dest: 'checks/epic-open.sh', exec: true },
|
|
445
477
|
{ src: 'skills/yad-checks/templates/checks/reconcile-debt-check.sh', dest: 'checks/reconcile-debt-check.sh', exec: true },
|
|
478
|
+
// E120. Checks the Product out in CI where .sdlc/product-link.json says, before the four gates above read it.
|
|
479
|
+
{ src: 'skills/yad-checks/templates/checks/product-checkout.sh', dest: 'checks/product-checkout.sh', exec: true },
|
|
446
480
|
{ src: 'skills/yad-checks/templates/checks/verified-commits.sh', dest: 'checks/verified-commits.sh', exec: true },
|
|
447
481
|
{ src: 'skills/yad-checks/templates/checks/commit-message.sh', dest: 'checks/commit-message.sh', exec: true },
|
|
448
482
|
// E65. The CHECK is ours and wired; the map it reads (`.sdlc/risk-map`) is the team's and never is.
|
|
@@ -471,6 +505,13 @@ export const REPO_WIRING = {
|
|
|
471
505
|
// else is a local edit, reported as `modified` and left alone (#164). Committed, so the record
|
|
472
506
|
// travels with the repo instead of living in one person's clone.
|
|
473
507
|
export const MANAGED_LEDGER = '.sdlc/managed.json';
|
|
508
|
+
// E120. Where a connected code repo's Product lives, as a fact of the repo — `git_url`, `path` (from the
|
|
509
|
+
// repo root: where CI checks the Product out) and `default_branch`. Written by `yad check --fix` /
|
|
510
|
+
// `yad update` (the Product knows both sides), merged on its own, and read from the BASE by the four
|
|
511
|
+
// Product-reading gates and checks/product-checkout.sh. NOT `.sdlc/product.json`: that is the Product's
|
|
512
|
+
// own settings file, and a code repo holding one would be taken for a Product.
|
|
513
|
+
export const PRODUCT_LINK = '.sdlc/product-link.json';
|
|
514
|
+
export const PRODUCT_LINK_DEFAULT_PATH = '.yad/product';
|
|
474
515
|
// Suffix for the copy written beside a managed file before its content is replaced without that
|
|
475
516
|
// proof — the local edit is always recoverable from the working tree, not only from git history.
|
|
476
517
|
export const BACKUP_SUFFIX = '.yad-orig';
|
|
@@ -481,31 +522,33 @@ export const wiringFor = (platform) => [
|
|
|
481
522
|
];
|
|
482
523
|
|
|
483
524
|
// Product wiring: CI installed on the PRODUCT itself (dest is the project root — the Product IS the
|
|
484
|
-
// root). Installed only when
|
|
485
|
-
//
|
|
486
|
-
//
|
|
525
|
+
// root). Installed only when the Product settings (product.json, or hub.json until v5) name a
|
|
526
|
+
// platform and the ledger is verified. Carries the merge-time gate sync (a review PR's merge, and a
|
|
527
|
+
// 15-minute schedule that catches a merge the event missed, run `yad gate ci --merged`; nothing fires
|
|
528
|
+
// on an approval) and the verified-commits gate (no commit without a platform-Verified signature
|
|
529
|
+
// reaches merge on the Product).
|
|
487
530
|
export const PRODUCT_WIRING = {
|
|
488
531
|
common: [
|
|
489
532
|
{ src: 'skills/yad-checks/templates/checks/verified-commits.sh', dest: 'checks/verified-commits.sh', exec: true },
|
|
490
533
|
// The ledger is CI-owned: block non-bot commits to gate-state files on the Product review PRs.
|
|
491
534
|
{ src: 'skills/yad-checks/templates/checks/ledger-guard.sh', dest: 'checks/ledger-guard.sh', exec: true },
|
|
492
|
-
// Pattern gates run on the Product too (profile:
|
|
535
|
+
// Pattern gates run on the Product too (profile: product) — commit subject + PR title + PR body.
|
|
493
536
|
{ src: 'skills/yad-checks/templates/checks/commit-message.sh', dest: 'checks/commit-message.sh', exec: true },
|
|
494
537
|
{ src: 'skills/yad-pr-template/templates/checks/pr-title.sh', dest: 'checks/pr-title.sh', exec: true },
|
|
495
538
|
{ src: 'skills/yad-pr-template/templates/checks/pr-template.sh', dest: 'checks/pr-template.sh', exec: true },
|
|
496
539
|
],
|
|
497
540
|
github: [
|
|
498
|
-
{ src: 'skills/yad-
|
|
541
|
+
{ src: 'skills/yad-product-bridge/templates/github/yad-gate-sync.yml', dest: '.github/workflows/yad-gate-sync.yml' },
|
|
499
542
|
{ src: 'skills/yad-checks/templates/github/yad-verified-commits.yml', dest: '.github/workflows/yad-verified-commits.yml' },
|
|
500
|
-
{ src: 'skills/yad-checks/templates/github/yad-
|
|
543
|
+
{ src: 'skills/yad-checks/templates/github/yad-product-checks.yml', dest: '.github/workflows/yad-product-checks.yml' },
|
|
501
544
|
// Integrity gate for the Product's own direct-to-default pushes (`yad update --push`; the machine-
|
|
502
545
|
// state `yad checkpoint`/`gate ci` commits carry [skip ci] and are intentionally not re-checked).
|
|
503
546
|
{ src: 'skills/yad-checks/templates/github/yad-update-guard.yml', dest: '.github/workflows/yad-update-guard.yml' },
|
|
504
547
|
],
|
|
505
548
|
gitlab: [
|
|
506
|
-
{ src: 'skills/yad-
|
|
549
|
+
{ src: 'skills/yad-product-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml', dest: '.gitlab/ci/yad-gate-sync.yml' },
|
|
507
550
|
{ src: 'skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml', dest: '.gitlab/ci/yad-verified-commits.yml' },
|
|
508
|
-
{ src: 'skills/yad-checks/templates/gitlab/yad-
|
|
551
|
+
{ src: 'skills/yad-checks/templates/gitlab/yad-product-checks.gitlab-ci.yml', dest: '.gitlab/ci/yad-product-checks.yml' },
|
|
509
552
|
{ src: 'skills/yad-checks/templates/gitlab/yad-update-guard.gitlab-ci.yml', dest: '.gitlab/ci/yad-update-guard.yml' },
|
|
510
553
|
],
|
|
511
554
|
};
|
|
@@ -513,10 +556,26 @@ export const PRODUCT_WIRING = {
|
|
|
513
556
|
// Harness hooks: the LOCAL half of the ledger rule, installed on the Product beside the CI gates and
|
|
514
557
|
// active under the same verified-ledger predicate (#171). Kept out of `PRODUCT_WIRING` because a hook is not a
|
|
515
558
|
// CI gate — it is advisory, fails open, and its adapter (below) is per-harness, not per-platform.
|
|
559
|
+
//
|
|
560
|
+
// Node scripts, not shell scripts (E113): one file runs on Windows, macOS and Linux, and a harness entry
|
|
561
|
+
// runs it as `node <file>`, so no execute bit is needed — Windows cannot keep one on disk.
|
|
562
|
+
// The branch-protection setup guide (E48), linked from `yad doctor`'s `protection` section. A URL, not a
|
|
563
|
+
// path: the reader is in their own repo, where `docs/` is not yadflow's.
|
|
564
|
+
export const PROTECTION_GUIDE_URL = 'https://github.com/abdelrahmannasr/yadflow/blob/main/docs/branch-protection.md';
|
|
565
|
+
// The hub -> Product hand-change guide (E124), linked from `yad migrate`'s last line while `.sdlc/hub.json` is on
|
|
566
|
+
// disk. A URL for the same reason.
|
|
567
|
+
export const PRODUCT_RENAME_GUIDE_URL = 'https://github.com/abdelrahmannasr/yadflow/blob/main/docs/migrations/hub-to-product.md';
|
|
568
|
+
|
|
516
569
|
export const HOOK_WIRING = [
|
|
517
|
-
{ src: 'skills/yad-checks/templates/hooks/ledger-guard.
|
|
570
|
+
{ src: 'skills/yad-checks/templates/hooks/ledger-guard.mjs', dest: 'hooks/ledger-guard.mjs' },
|
|
518
571
|
];
|
|
519
572
|
|
|
573
|
+
// The shell scripts every hook was before E113. `yad check --fix` rewrites an entry that still names one
|
|
574
|
+
// (each adapter's `legacyCommands`) and then deletes the script — but only a copy the provenance record
|
|
575
|
+
// proves yad wrote and nobody edited. An edited or unrecorded copy is the team's, and may be what their
|
|
576
|
+
// own hand-wired hook runs, so it is left where it is.
|
|
577
|
+
export const LEGACY_HOOK_SCRIPTS = Object.freeze(['hooks/ledger-guard.sh', 'hooks/ledger-guard-cursor.sh', 'hooks/yad-capture.sh']);
|
|
578
|
+
|
|
520
579
|
// Per-harness adapter config: which IDE target gets a hook entry written, where, and in what shape.
|
|
521
580
|
//
|
|
522
581
|
// A harness qualifies for an entry here ONLY if it can run a command BEFORE a file is written and
|
|
@@ -526,10 +585,10 @@ export const HOOK_WIRING = [
|
|
|
526
585
|
// Cursor — so "no entry here" means "not wired", never "checked and found wanting":
|
|
527
586
|
//
|
|
528
587
|
// .claude Claude Code — `PreToolUse` in `.claude/settings.json`, reading the EXIT CODE, so the
|
|
529
|
-
// entry points straight at the shared `hooks/ledger-guard.
|
|
588
|
+
// entry points straight at the shared `hooks/ledger-guard.mjs`.
|
|
530
589
|
// .cursor Cursor — `preToolUse` in `.cursor/hooks.json`, a PERMISSION hook that reads a JSON
|
|
531
590
|
// 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.
|
|
591
|
+
// nothing when it allows, so this entry points at `hooks/ledger-guard-cursor.mjs`
|
|
533
592
|
// instead (the `wiring` below), which speaks that protocol.
|
|
534
593
|
//
|
|
535
594
|
// Cursor's `afterFileEdit` is deliberately NOT used: it fires once the edit is already on disk, so it
|
|
@@ -554,7 +613,9 @@ export const HOOK_WIRING = [
|
|
|
554
613
|
// guard that is off while every report says it is on.
|
|
555
614
|
const CLAUDE_PROJECT_DIR_ENV = 'CLAUDE_PROJECT_DIR';
|
|
556
615
|
const CURSOR_PROJECT_DIR_ENV = 'CURSOR_PROJECT_DIR';
|
|
557
|
-
|
|
616
|
+
// `node` then the script: the harness runs the command through a shell (Git Bash on Windows for Claude
|
|
617
|
+
// Code), and `node` is on PATH wherever yadflow runs at all.
|
|
618
|
+
const guardCommand = (envVar) => `node "$${envVar}/hooks/ledger-guard.mjs"`;
|
|
558
619
|
|
|
559
620
|
export const HOOK_ADAPTERS = Object.freeze({
|
|
560
621
|
'.claude': Object.freeze({
|
|
@@ -581,7 +642,10 @@ export const HOOK_ADAPTERS = Object.freeze({
|
|
|
581
642
|
// is ours to normalise; anything else is the team's, even if it names a similar path. Never widen
|
|
582
643
|
// this to a substring test — a team keeping its own wrapper at `.claude/hooks/ledger-guard.sh`
|
|
583
644
|
// would have their hook silently rewritten to ours.
|
|
584
|
-
legacyCommands: Object.freeze([
|
|
645
|
+
legacyCommands: Object.freeze([
|
|
646
|
+
'$CLAUDE_PROJECT_DIR/hooks/ledger-guard.sh', // 3.16.x, pre-quoting
|
|
647
|
+
'"$CLAUDE_PROJECT_DIR/hooks/ledger-guard.sh"', // the bash script, before E113
|
|
648
|
+
]),
|
|
585
649
|
}),
|
|
586
650
|
'.cursor': Object.freeze({
|
|
587
651
|
target: '.cursor',
|
|
@@ -600,36 +664,38 @@ export const HOOK_ADAPTERS = Object.freeze({
|
|
|
600
664
|
// Cursor's `preToolUse` is a PERMISSION hook: it answers in JSON on stdout, and empty stdout is
|
|
601
665
|
// invalid JSON, which BLOCKS. So the plain guard cannot be wired here — it prints nothing when it
|
|
602
666
|
// allows, which would have blocked every file write in a verified project. This wrapper, installed
|
|
603
|
-
// with the adapter below, always prints a permission answer.
|
|
604
|
-
//
|
|
667
|
+
// with the adapter below, always prints a permission answer. The script itself takes no arguments:
|
|
668
|
+
// the entry stays a program and one path, the one form Cursor's own docs show.
|
|
605
669
|
wiring: Object.freeze([
|
|
606
|
-
Object.freeze({ src: 'skills/yad-checks/templates/hooks/ledger-guard-cursor.
|
|
670
|
+
Object.freeze({ src: 'skills/yad-checks/templates/hooks/ledger-guard-cursor.mjs', dest: 'hooks/ledger-guard-cursor.mjs' }),
|
|
607
671
|
]),
|
|
608
672
|
// RELATIVE, and deliberately not `$CURSOR_PROJECT_DIR/…` — the opposite of the Claude entry above,
|
|
609
673
|
// for a documented reason. Cursor's docs say a project hook RUNS FROM THE PROJECT ROOT, which is
|
|
610
674
|
// the directory holding the `.cursor/hooks.json` this entry lives in, and the same directory
|
|
611
|
-
// holding `hooks/ledger-guard.
|
|
675
|
+
// holding `hooks/ledger-guard.mjs`. So the relative path always resolves, whatever the harness's
|
|
612
676
|
// notion of cwd elsewhere.
|
|
613
677
|
//
|
|
614
|
-
// What they do NOT say is
|
|
615
|
-
// no variable and no quotes here: `$CURSOR_PROJECT_DIR`
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
678
|
+
// What they do NOT say is WHICH shell runs the command — on Windows it is PowerShell. That is why
|
|
679
|
+
// there is no variable and no quotes here: `$CURSOR_PROJECT_DIR` is not an environment variable to
|
|
680
|
+
// PowerShell, and a command that begins with a quoted path is not a command to it either. Every one
|
|
681
|
+
// of those failures is silent, fails OPEN, and leaves `yad doctor` truthfully reporting the entry
|
|
682
|
+
// as wired while nothing is ever refused.
|
|
683
|
+
//
|
|
684
|
+
// E113: `node` and a relative script. Cursor's docs call the command a "shell string" and show
|
|
685
|
+
// `python3 .cursor/hooks/x.py`, so a program and one argument is a documented form — and on Windows
|
|
686
|
+
// it is the one that runs at all: PowerShell cannot run a `.sh` file.
|
|
620
687
|
projectDirEnv: CURSOR_PROJECT_DIR_ENV,
|
|
621
|
-
command: 'hooks/ledger-guard-cursor.
|
|
688
|
+
command: 'node hooks/ledger-guard-cursor.mjs',
|
|
622
689
|
// This harness answers with a JSON verdict and treats an empty or off-schema answer as a REFUSAL.
|
|
623
690
|
// `yad doctor` uses the flag to catch a hand-wired command that would fail closed — see
|
|
624
691
|
// `miswiredGuardCommand`. An adapter without it reads the exit code, where a silent command is
|
|
625
692
|
// simply a guard that never fires.
|
|
626
693
|
requiresJsonVerdict: true,
|
|
627
|
-
|
|
628
|
-
legacyCommands: Object.freeze([]),
|
|
694
|
+
legacyCommands: Object.freeze(['hooks/ledger-guard-cursor.sh']), // the bash script, before E113
|
|
629
695
|
}),
|
|
630
696
|
});
|
|
631
697
|
|
|
632
|
-
// Background capture (E43): the POST-edit half. After an agent writes a file, `hooks/yad-capture.
|
|
698
|
+
// Background capture (E43): the POST-edit half. After an agent writes a file, `hooks/yad-capture.mjs` runs
|
|
633
699
|
// `yad capture --hook`, which snapshots every changed artifact onto the person's private
|
|
634
700
|
// `yad/wip/<name>/<epic>` branches. Unlike the guard it is wired in BOTH ledger modes — capture is about
|
|
635
701
|
// never losing a draft, which matters whoever owns the ledger — and on any Product whose config does not
|
|
@@ -637,7 +703,7 @@ export const HOOK_ADAPTERS = Object.freeze({
|
|
|
637
703
|
// check serve both; `observe: true` marks a hook that only watches (it cannot refuse), so its entry carries
|
|
638
704
|
// no `failClosed`, and a null `matcher` is an event that takes none (Cursor's `afterFileEdit`).
|
|
639
705
|
export const CAPTURE_WIRING = [
|
|
640
|
-
{ src: 'skills/yad-checks/templates/hooks/yad-capture.
|
|
706
|
+
{ src: 'skills/yad-checks/templates/hooks/yad-capture.mjs', dest: 'hooks/yad-capture.mjs' },
|
|
641
707
|
];
|
|
642
708
|
export const CAPTURE_ADAPTERS = Object.freeze({
|
|
643
709
|
'.claude': Object.freeze({
|
|
@@ -651,10 +717,11 @@ export const CAPTURE_ADAPTERS = Object.freeze({
|
|
|
651
717
|
matcher: 'Edit|Write|MultiEdit|NotebookEdit',
|
|
652
718
|
projectDirEnv: CLAUDE_PROJECT_DIR_ENV,
|
|
653
719
|
// Quoted and anchored on the project root, for the reason the guard's entry is (see above).
|
|
654
|
-
|
|
720
|
+
// `--format claude` is said here, not guessed by the script: Cursor sets `CLAUDE_PROJECT_DIR` too.
|
|
721
|
+
command: `node "$${CLAUDE_PROJECT_DIR_ENV}/hooks/yad-capture.mjs" --format claude`,
|
|
655
722
|
observe: true,
|
|
656
723
|
wiring: Object.freeze([]),
|
|
657
|
-
legacyCommands: Object.freeze([]),
|
|
724
|
+
legacyCommands: Object.freeze([`"$${CLAUDE_PROJECT_DIR_ENV}/hooks/yad-capture.sh"`]), // before E113
|
|
658
725
|
}),
|
|
659
726
|
'.cursor': Object.freeze({
|
|
660
727
|
target: '.cursor',
|
|
@@ -668,10 +735,10 @@ export const CAPTURE_ADAPTERS = Object.freeze({
|
|
|
668
735
|
matcher: null,
|
|
669
736
|
projectDirEnv: CURSOR_PROJECT_DIR_ENV,
|
|
670
737
|
// Relative and unquoted, for the reason the guard's Cursor entry is (see above).
|
|
671
|
-
command: 'hooks/yad-capture.
|
|
738
|
+
command: 'node hooks/yad-capture.mjs',
|
|
672
739
|
observe: true,
|
|
673
740
|
wiring: Object.freeze([]),
|
|
674
|
-
legacyCommands: Object.freeze([]),
|
|
741
|
+
legacyCommands: Object.freeze(['hooks/yad-capture.sh']), // before E113
|
|
675
742
|
}),
|
|
676
743
|
});
|
|
677
744
|
|