yadflow 4.0.0-next.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +699 -0
  2. package/README.md +85 -22
  3. package/bin/commands.mjs +3 -1
  4. package/bin/yad.mjs +183 -29
  5. package/cli/capture.mjs +4 -2
  6. package/cli/checkpoint.mjs +10 -10
  7. package/cli/claims.mjs +4 -4
  8. package/cli/commit.mjs +48 -12
  9. package/cli/docs.mjs +15 -13
  10. package/cli/doctor.mjs +557 -141
  11. package/cli/epic-state.mjs +41 -18
  12. package/cli/errors.mjs +3 -2
  13. package/cli/find-product.mjs +197 -0
  14. package/cli/fold.mjs +4 -4
  15. package/cli/gate-local.mjs +13 -13
  16. package/cli/gate.mjs +111 -103
  17. package/cli/history.mjs +13 -13
  18. package/cli/hook.mjs +201 -30
  19. package/cli/index-command.mjs +6 -6
  20. package/cli/lib.mjs +91 -29
  21. package/cli/manifest.mjs +171 -104
  22. package/cli/migrate.mjs +208 -17
  23. package/cli/mode.mjs +22 -22
  24. package/cli/next.mjs +6 -6
  25. package/cli/openpr.mjs +32 -24
  26. package/cli/people.mjs +22 -10
  27. package/cli/plan.mjs +718 -45
  28. package/cli/platform.mjs +13 -7
  29. package/cli/product-index.mjs +4 -4
  30. package/cli/{hubcommit.mjs → productcommit.mjs} +11 -7
  31. package/cli/protection.mjs +19 -9
  32. package/cli/reconcile.mjs +98 -23
  33. package/cli/repo-publish.mjs +22 -16
  34. package/cli/repo.mjs +128 -33
  35. package/cli/report.mjs +4 -2
  36. package/cli/review.mjs +26 -19
  37. package/cli/setup.mjs +105 -36
  38. package/cli/ship.mjs +1 -1
  39. package/cli/skip.mjs +4 -4
  40. package/cli/tidy.mjs +7 -7
  41. package/cli/update-commit.mjs +22 -19
  42. package/cli/workspace.mjs +495 -0
  43. package/package.json +7 -4
  44. package/skills/sdlc/config.yaml +14 -13
  45. package/skills/sdlc/module-help.csv +8 -8
  46. package/skills/yad-analysis/SKILL.md +6 -4
  47. package/skills/yad-architecture/SKILL.md +4 -3
  48. package/skills/yad-backfill/SKILL.md +11 -5
  49. package/skills/yad-backfill/references/backfill.md +33 -3
  50. package/skills/yad-backfill/templates/checks/backfill-check.sh +200 -17
  51. package/skills/yad-change/SKILL.md +1 -1
  52. package/skills/yad-checks/SKILL.md +48 -27
  53. package/skills/yad-checks/references/check-gates.md +213 -53
  54. package/skills/yad-checks/templates/checks/commit-message.sh +39 -13
  55. package/skills/yad-checks/templates/checks/contract-check.sh +472 -23
  56. package/skills/yad-checks/templates/checks/epic-open.sh +398 -8
  57. package/skills/yad-checks/templates/checks/ledger-guard.sh +75 -25
  58. package/skills/yad-checks/templates/checks/lineage-check.sh +391 -7
  59. package/skills/yad-checks/templates/checks/product-checkout.sh +141 -0
  60. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +399 -8
  61. package/skills/yad-checks/templates/checks/risk-map-check.sh +35 -3
  62. package/skills/yad-checks/templates/checks/spec-link.sh +36 -3
  63. package/skills/yad-checks/templates/checks/verified-commits.sh +26 -2
  64. package/skills/yad-checks/templates/github/yad-checks.yml +33 -13
  65. package/skills/yad-checks/templates/github/{yad-hub-checks.yml → yad-product-checks.yml} +14 -10
  66. package/skills/yad-checks/templates/github/yad-update-guard.yml +1 -1
  67. package/skills/yad-checks/templates/github/yad-verified-commits.yml +1 -1
  68. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +12 -0
  69. package/skills/yad-checks/templates/gitlab/{yad-hub-checks.gitlab-ci.yml → yad-product-checks.gitlab-ci.yml} +16 -16
  70. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +2 -2
  71. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.mjs +130 -0
  72. package/skills/yad-checks/templates/hooks/ledger-guard.mjs +117 -0
  73. package/skills/yad-checks/templates/hooks/yad-capture.mjs +117 -0
  74. package/skills/yad-commit/SKILL.md +18 -7
  75. package/skills/yad-connect-design/SKILL.md +1 -1
  76. package/skills/yad-connect-design/references/design-registry.md +1 -1
  77. package/skills/yad-connect-docs/SKILL.md +16 -11
  78. package/skills/yad-connect-docs/references/docs-registry.md +8 -8
  79. package/skills/yad-connect-learning/SKILL.md +1 -1
  80. package/skills/yad-connect-learning/references/learning-registry.md +1 -1
  81. package/skills/yad-connect-repos/SKILL.md +31 -8
  82. package/skills/yad-connect-repos/references/code-context.md +1 -1
  83. package/skills/yad-connect-repos/references/{hub-config.md → product-config.md} +19 -11
  84. package/skills/yad-connect-repos/references/repos-registry.md +1 -1
  85. package/skills/yad-connect-testing/SKILL.md +1 -1
  86. package/skills/yad-connect-testing/references/testing-registry.md +1 -1
  87. package/skills/yad-discovery/SKILL.md +2 -2
  88. package/skills/yad-discovery/references/discovery-schema.md +1 -1
  89. package/skills/yad-docs/SKILL.md +1 -1
  90. package/skills/yad-docs/references/data-mapping.md +1 -1
  91. package/skills/yad-docs/templates/app/package-lock.json +8 -8
  92. package/skills/yad-docs/templates/app/package.json +1 -1
  93. package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +2 -2
  94. package/skills/yad-docs-overview/references/pipeline-model.md +3 -3
  95. package/skills/yad-engineer-review/SKILL.md +2 -2
  96. package/skills/yad-engineer-review/references/ship-and-record.md +3 -3
  97. package/skills/yad-epic/SKILL.md +6 -4
  98. package/skills/yad-epic/references/state-schema.md +13 -12
  99. package/skills/yad-open-pr/SKILL.md +1 -1
  100. package/skills/yad-pair-review/SKILL.md +5 -5
  101. package/skills/yad-pair-review/references/session-state.md +2 -2
  102. package/skills/yad-pr-template/SKILL.md +14 -12
  103. package/skills/yad-pr-template/templates/checks/pr-template.sh +18 -16
  104. package/skills/yad-pr-template/templates/checks/pr-title.sh +15 -13
  105. package/skills/yad-pr-template/templates/{hub → product}/github/pull_request_template.md +2 -2
  106. package/skills/yad-pr-template/templates/{hub → product}/gitlab/merge_request_templates/Default.md +2 -2
  107. package/skills/{yad-hub-bridge → yad-product-bridge}/SKILL.md +15 -15
  108. package/skills/{yad-hub-bridge → yad-product-bridge}/references/bridge.md +15 -10
  109. package/skills/{yad-hub-bridge → yad-product-bridge}/references/login-roster.md +2 -2
  110. package/skills/{yad-hub-bridge/templates/checks/hub-route.sh → yad-product-bridge/templates/checks/product-route.sh} +2 -2
  111. package/skills/{yad-hub-bridge → yad-product-bridge}/templates/github/yad-gate-sync.yml +29 -16
  112. package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/gitlab-ci.include-root.yml +1 -1
  113. package/skills/{yad-hub-bridge → yad-product-bridge}/templates/gitlab/yad-gate-sync.gitlab-ci.yml +5 -3
  114. package/skills/yad-report/SKILL.md +1 -1
  115. package/skills/yad-review-companion/SKILL.md +3 -3
  116. package/skills/yad-review-gate/SKILL.md +22 -23
  117. package/skills/yad-review-gate/references/gating.md +13 -13
  118. package/skills/yad-run/SKILL.md +2 -2
  119. package/skills/yad-run/references/run-loop.md +2 -2
  120. package/skills/yad-spec/SKILL.md +2 -1
  121. package/skills/yad-spec/references/spec-handoff.md +19 -10
  122. package/skills/yad-stories/SKILL.md +2 -2
  123. package/skills/yad-sync-repos/SKILL.md +19 -3
  124. package/skills/yad-test-cases/SKILL.md +2 -2
  125. package/skills/yad-ui/SKILL.md +2 -2
  126. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +0 -91
  127. package/skills/yad-checks/templates/hooks/ledger-guard.sh +0 -100
  128. package/skills/yad-checks/templates/hooks/yad-capture.sh +0 -53
package/cli/lib.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // Shared helpers for the `yad` CLI. Node >=18 built-ins only — no dependencies.
2
- import { createHash } from 'node:crypto';
2
+ import { createHash, randomBytes } from 'node:crypto';
3
3
  import { err } from './errors.mjs';
4
- import { MIRRORED_FILES, SCHEMA_VERSION, VERSION } from './manifest.mjs';
4
+ import { MIRRORED_FILES, SCHEMA_VERSION, VERSION, mirrorDrift, preferring } from './manifest.mjs';
5
5
  import { spawnSync } from 'node:child_process';
6
6
  import * as readline from 'node:readline/promises';
7
7
  import { stdin as input, stdout as output } from 'node:process';
@@ -24,7 +24,19 @@ export const PKG_ROOT = fileURLToPath(new URL('../', import.meta.url));
24
24
  // holds the one object and nothing else — a progress line printed before the answer would otherwise
25
25
  // make the whole output unparseable. Each `warn` is also collected into `warnings`, as plain text.
26
26
  // A command never calls JSON.stringify toward stdout itself; a test greps for it.
27
- export const JSON_VERSION = 1;
27
+ // Text read from a shared file (a repo name, a path) is shown in the terminal: every control character
28
+ // (C0, DEL and C1 — U+0080–U+009F, which some terminals obey too) becomes a space, so a registry entry cannot move the cursor or recolour what follows.
29
+ // So do the line and paragraph separators and the bidi controls, which can make a line read in another
30
+ // order than it holds (E81 review 9).
31
+ const INVISIBLE = new Set([0x2028, 0x2029, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d, 0x202e, 0x2066, 0x2067, 0x2068, 0x2069, 0x061c]);
32
+ export const forTerminal = (s) => [...String(s ?? '')].map((ch) => { const n = ch.charCodeAt(0); return n < 32 || (n >= 127 && n <= 159) || INVISIBLE.has(n) ? ' ' : ch; }).join('');
33
+
34
+ // A registry name inside a command the person is invited to copy: as it is only when it is one plain
35
+ // word that starts with a letter or digit (never an option like `--push`), else `<name>` — the shared
36
+ // file could hold `api; curl …|sh` (E81 reviews 9–10; here since review 15, for doctor and people.mjs).
37
+ export const asArg = (name) => (typeof name === 'string' && /^\w[\w.-]*$/.test(name) ? name : '<name>');
38
+
39
+ export const JSON_VERSION = 2;
28
40
  export const ENVELOPE_KEYS = ['jsonVersion', 'version', 'command'];
29
41
  let jsonRun = null;
30
42
  export function beginJSON(command) { jsonRun = { command, emitted: false, warnings: [], failures: [] }; }
@@ -151,14 +163,39 @@ export async function askYesNo(question, def = true) {
151
163
  export const asset = (...p) => path.join(PKG_ROOT, ...p);
152
164
  export const exists = (p) => fs.existsSync(p);
153
165
 
166
+ // Do two paths name the same existing file or folder? Used where one path comes from git and one from Node,
167
+ // which can spell the same place differently: on Windows git answers with the long name while Node's temp
168
+ // folder is often an 8.3 short name (`C:\Users\RUNNER~1\…`) that the JS realpath keeps; `.native` asks
169
+ // Windows for the final name. Windows also compares without case, and a macOS `/var` is `/private/var`.
170
+ // A path that does not exist is never the same as anything (E48).
171
+ export function samePath(a, b, platform = process.platform) {
172
+ const real = (p) => { try { return fs.realpathSync.native(p); } catch { return null; } };
173
+ const ra = real(a);
174
+ const rb = real(b);
175
+ if (ra === null || rb === null) return false;
176
+ return platform === 'win32' ? ra.toLowerCase() === rb.toLowerCase() : ra === rb;
177
+ }
178
+
154
179
  export function fileSha(p) {
155
180
  if (!fs.existsSync(p)) return null;
156
181
  return 'sha256:' + createHash('sha256').update(fs.readFileSync(p)).digest('hex');
157
182
  }
158
- // True when dest exists and its bytes match src exactly.
183
+ // The sha of a file yad INSTALLS, read the way git would give it back: every CRLF counted as LF (E113).
184
+ // On Windows git checks text out with CRLF (`core.autocrlf`, on by default there), so a teammate's clone
185
+ // holds each shipped file with different bytes and the same content — and a byte compare would call every
186
+ // one of them outdated, or edited, for ever. A file with no CR in it hashes exactly as `fileSha` does, so
187
+ // every sha already recorded in `.sdlc/managed.json` still matches. Not for artifact hashes: those are
188
+ // `fileSha`, and an approval is bound to them.
189
+ export function contentSha(p) {
190
+ if (!fs.existsSync(p)) return null;
191
+ const bytes = fs.readFileSync(p);
192
+ const text = bytes.includes(13) ? Buffer.from(bytes.toString('latin1').replace(/\r\n/g, '\n'), 'latin1') : bytes;
193
+ return 'sha256:' + createHash('sha256').update(text).digest('hex');
194
+ }
195
+ // True when dest exists and its content matches src, line endings aside (see `contentSha`).
159
196
  export function sameContent(src, dest) {
160
- const a = fileSha(src);
161
- const b = fileSha(dest);
197
+ const a = contentSha(src);
198
+ const b = contentSha(dest);
162
199
  return a !== null && a === b;
163
200
  }
164
201
 
@@ -276,13 +313,28 @@ export function writeJSON(p, obj) {
276
313
  try {
277
314
  if (fs.readFileSync(p, 'utf8') === data) return;
278
315
  } catch { /* missing or unreadable — fall through and write it */ }
316
+ writeFileAtomic(p, data);
317
+ }
318
+
319
+ // Write `data` to `p` through a fresh temporary file and a rename: a crash leaves the old file or the new
320
+ // one, never half of either, and a link standing at `p` is replaced rather than followed.
321
+ export function writeFileAtomic(p, data) {
279
322
  fs.mkdirSync(path.dirname(p), { recursive: true });
280
- const tmp = `${p}.${process.pid}.tmp`;
281
- fs.writeFileSync(tmp, data);
323
+ // A name nobody can guess, created exclusively (`wx`): a Product can commit `repos.json.<pid>.tmp` as a
324
+ // link, and a plain write follows it — a dangling one too — putting this file's text wherever the link
325
+ // points (`~/.zshenv`; E81 review 17). `wx` refuses any entry already there, links included.
326
+ const tmp = `${p}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
327
+ try { fs.writeFileSync(tmp, data, { flag: 'wx' }); }
328
+ catch (e) {
329
+ // A half-written file of our own (a full disk) is removed; an entry that was already there (EEXIST)
330
+ // is not ours to delete.
331
+ if (e.code !== 'EEXIST') { try { fs.rmSync(tmp, { force: true }); } catch { /* keep the write's own error */ } }
332
+ throw e;
333
+ }
282
334
  try {
283
335
  fs.renameSync(tmp, p);
284
336
  } catch (e) {
285
- fs.rmSync(tmp, { force: true });
337
+ try { fs.rmSync(tmp, { force: true }); } catch { /* keep the rename's own error */ }
286
338
  throw e;
287
339
  }
288
340
  }
@@ -304,6 +356,18 @@ export function run(cmd, args = [], opts = {}) {
304
356
  }
305
357
  export const has = (cmd) => run(process.platform === 'win32' ? 'where' : 'which', [cmd]).ok;
306
358
 
359
+ // An npm launcher (`npm`, `npx`) is a `.cmd` file on Windows, which Node will not start without a shell
360
+ // (since 18.20.2) — a plain spawn fails with EINVAL there. So on Windows it runs through one, each word in
361
+ // double quotes (a Windows path cannot hold a `"`); everywhere else it is spawned directly (E113).
362
+ export function launcherInvocation(cmd, args, platform = process.platform) {
363
+ if (platform !== 'win32') return { cmd, args, shell: false };
364
+ return { cmd: [cmd, ...args].map((a) => `"${String(a).replace(/"/g, '')}"`).join(' '), args: [], shell: true };
365
+ }
366
+ export function runLauncher(cmd, args = [], opts = {}) {
367
+ const inv = launcherInvocation(cmd, args);
368
+ return run(inv.cmd, inv.args, { ...opts, ...(inv.shell ? { shell: true } : {}) });
369
+ }
370
+
307
371
  // Push HEAD to origin/<target>, rebasing onto it and retrying on rejection — both the Shape gate
308
372
  // sync and the Build checkpoint push append-only ledgers to the default branch, so a concurrent
309
373
  // push is a normal race, not an error. Returns { ok } after up to `attempts` tries; logs each retry.
@@ -320,26 +384,23 @@ export function pushWithRebase(cwd, target, { attempts = 3 } = {}) {
320
384
  return { ok: false };
321
385
  }
322
386
 
323
- // ---- the product config, which lives under two names for one major -----------------------------
387
+ // ---- the product config, which lives under two names until v5 ----------------------------------
324
388
  //
325
- // Read through `productConfigPath` (manifest.mjs), which every reader in cli/ now does. Write
326
- // through here: BOTH names, every time.
389
+ // Read through `productConfigPath` (manifest.mjs), which every reader in cli/ does. Write through
390
+ // here: BOTH names, every time.
327
391
  //
328
- // The duplicate is not sloppiness. `templates/checks/ledger-guard.sh` sits committed inside the
329
- // user's own repository and opens `.sdlc/hub.json` by that literal path; it is refreshed by
330
- // `yad update`, which is a separate act from `yad migrate`. So a migrated-but-not-updated project is
331
- // a real state, and if only the new name existed its guard would find nothing, read no platform,
332
- // call the ledger local, and stop rejecting human commits to it. An upgrade that silently disarms a
333
- // safety gate is worse than a duplicated file.
392
+ // The duplicate is not sloppiness. The check gates sit committed inside the user's own repository,
393
+ // and the ones installed before E122 open `.sdlc/hub.json` by that literal path; they are refreshed by
394
+ // `yad update`, which is a separate act from `yad migrate`. So a project whose gates are older than
395
+ // its CLI is a real state, and if only the new name existed its `ledger-guard` would find nothing,
396
+ // read no platform, call the ledger local, and stop rejecting human commits to it. An upgrade that
397
+ // silently disarms a safety gate is worse than a duplicated file. v5 deletes the old name.
334
398
  //
335
- // Order matters, and it follows which file is AUTHORITATIVE — the old name, this major
336
- // (`productConfigPath`, cli/manifest.mjs). The new name is written first, so if the second write
337
- // fails the readers are all still on the untouched old copy: nothing has half-changed underneath
338
- // them. Writing the authoritative file first would leave every reader on new settings while the
339
- // mirror still says something else, which is the harder failure to notice.
399
+ // Order: the old name first, then the new one, which readers use (`preferring`, cli/manifest.mjs).
400
+ // If the second write fails, the pair disagrees and every command refuses until `yad migrate`
401
+ // settles it (`mirrorDrift`) — loud, never a silent half-change. The authoritative copy is the one
402
+ // left untouched in that case, so the refusal shows the settings every reader was already using.
340
403
  //
341
- // `yad doctor` reports the two copies disagreeing either way, so a half-written pair is visible
342
- // rather than silent.
343
404
  // Write a file that lives under two names. Returns the paths it actually wrote — empty when there
344
405
  // was nothing to do.
345
406
  //
@@ -352,19 +413,20 @@ export function pushWithRebase(cwd, target, { attempts = 3 } = {}) {
352
413
  // readable; only a genuine change writes, and then it writes both.
353
414
  export function writeMirrored(canonicalPath, legacyPath, obj) {
354
415
  // Compare against the AUTHORITATIVE copy — the same one readers use (`preferring`,
355
- // cli/manifest.mjs), which is the legacy name while it exists. Comparing against the other file
416
+ // cli/manifest.mjs), which is the new name whenever it exists. Comparing against the other file
356
417
  // would let the two rules disagree about whether anything changed.
357
- const readable = fs.existsSync(legacyPath) ? legacyPath : canonicalPath;
418
+ const readable = preferring(canonicalPath, legacyPath);
358
419
  const next = `${JSON.stringify(writeShape(canonicalPath, obj), null, 2)}\n`;
359
420
  // "Unchanged" means BOTH names are already right. A half-made pair — one side missing — is work
360
421
  // to do even when the readable copy matches, or the pair can never be repaired: the comparison
361
422
  // says nothing changed, nothing is written, and `yad doctor` reports the missing file for ever.
423
+ // A drifted pair fails the same test (the other copy is not `next`), so a write repairs it too.
362
424
  const bothPresent = fs.existsSync(canonicalPath) && fs.existsSync(legacyPath);
363
425
  try {
364
- if (bothPresent && fs.readFileSync(readable, 'utf8') === next) return [];
426
+ if (bothPresent && fs.readFileSync(readable, 'utf8') === next && !mirrorDrift(canonicalPath, legacyPath)) return [];
365
427
  } catch { /* unreadable — treat as a first write and fall through */ }
366
- writeJSON(canonicalPath, obj);
367
428
  writeJSON(legacyPath, obj);
429
+ writeJSON(canonicalPath, obj);
368
430
  return [canonicalPath, legacyPath];
369
431
  }
370
432