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/commit.mjs CHANGED
@@ -1,35 +1,48 @@
1
1
  // `yad commit` — commit by the SDLC conventions (CONTRIBUTING.md / config.yaml build).
2
2
  // Subject is Conventional Commits; trailers are emitted in the fixed order
3
- // Task -> Contract-Change -> Co-Authored-By. The human git author OWNS the commit; the AI is only a
4
- // co-author (flagged with --ai, or `none` for human-only). An atomic-commit guard keeps diffs small.
3
+ // Task -> Contract-Change -> Ledger-Override -> Co-Authored-By. The human git author OWNS the commit;
4
+ // the AI is only a co-author (flagged with --ai, or `none` for human-only). An atomic-commit guard keeps diffs small.
5
5
  // An explicit --task id is validated against the spec-link gate contract (<story>-T<NN>) so a
6
6
  // malformed trailer fails locally rather than after a push.
7
+ //
8
+ // `--manual --reason` (E49) is the door past the Product's local ledger hook (E48): the commit runs with
9
+ // the hook's skip switch, and the reason is recorded as a `Ledger-Override:` trailer so the reviewer —
10
+ // and the CI `ledger-guard` check, which quotes it — can see why. It opens this machine's hook and
11
+ // nothing more: CI still fails the commit, because anyone can type a trailer.
7
12
  import path from 'node:path';
8
13
  import fs from 'node:fs';
9
14
  import { c, log, ok, info, warn, fail, run, exists } from './lib.mjs';
10
15
  import {
11
16
  COMMIT_TYPES, AI_COAUTHORS, ATOMIC_FILE_LIMIT,
12
- TASK_TRAILER, CONTRACT_CHANGE_TRAILER, COAUTHOR_TRAILER, TASK_ID_RE,
17
+ TASK_TRAILER, CONTRACT_CHANGE_TRAILER, LEDGER_OVERRIDE_TRAILER, COAUTHOR_TRAILER, TASK_ID_RE,
13
18
  productConfigPath,
14
19
  } from './manifest.mjs';
20
+ import { OWNING_COMMIT_ENV, stagedLedgerHits } from './hook.mjs';
15
21
 
16
22
  // PURE — unit tested directly. Build the full commit message text.
17
- export function buildCommitMessage({ type, subject, task, contractChange = false, ai = 'none', body = '' }) {
23
+ export function buildCommitMessage({ type, subject, task, contractChange = false, ai = 'none', body = '', override = null }) {
18
24
  if (!COMMIT_TYPES.includes(type)) throw new Error(`invalid commit type "${type}" (one of: ${COMMIT_TYPES.join(', ')})`);
19
25
  if (!subject || !subject.trim()) throw new Error('commit subject is required');
20
26
  if (!(ai in AI_COAUTHORS)) throw new Error(`unknown --ai "${ai}" (one of: ${Object.keys(AI_COAUTHORS).join(', ')})`);
21
27
  if (/\.$/.test(subject.trim())) throw new Error('subject must not end with a period');
22
28
  // A malformed task id (e.g. a bare story `EP-x-S01` with no -T<NN>) commits fine locally but
23
29
  // then fails the spec-link CI gate — forcing a history rewrite + force-push. Reject it here so
24
- // it never enters a trailer. Well-formed branch-derived ids (taskFromBranch) are stricter and pass;
25
- // a lowercase-suffix branch is caught here, matching the case-sensitive spec-link grep.
30
+ // it never enters a trailer. A branch-derived id (taskFromBranch, which matches without case) is
31
+ // validated the same way, so `feat/EP-Demo-S01-T01` is caught here too, as spec-link would.
26
32
  if (task && !TASK_ID_RE.test(task)) {
27
- throw new Error(`invalid --task "${task}" (expected <story>-T<NN>, e.g. EP-x-S01-T02) — the spec-link CI gate would reject it`);
33
+ throw new Error(`invalid --task "${task}" (expected EP-<slug>-S<n>-T<NN> with a lowercase slug, e.g. EP-x-S01-T02) — the spec-link CI gate would reject it`);
34
+ }
35
+
36
+ // A trailer is one line: a newline in the reason would end it early and start a line git reads as
37
+ // body text or as a trailer of its own.
38
+ if (override !== null && (typeof override !== 'string' || !override.trim() || /[\r\n]/.test(override))) {
39
+ throw new Error('--reason must be one non-empty line (it becomes the Ledger-Override trailer)');
28
40
  }
29
41
 
30
42
  const trailers = [];
31
43
  if (task) trailers.push(`${TASK_TRAILER}: ${task}`);
32
44
  if (contractChange) trailers.push(`${CONTRACT_CHANGE_TRAILER}: yes`);
45
+ if (override !== null) trailers.push(`${LEDGER_OVERRIDE_TRAILER}: ${override.trim()}`);
33
46
  const co = AI_COAUTHORS[ai];
34
47
  if (co) trailers.push(`${COAUTHOR_TRAILER}: ${co.name} <${co.email}>`);
35
48
 
@@ -47,11 +60,31 @@ export function taskFromBranch(branch = '') {
47
60
 
48
61
  export async function runCommit(root, opts = {}) {
49
62
  log(c.bold('\nyad commit'));
50
- if (!exists(path.join(root, '.git'))) { fail('not a git repo'); process.exitCode = 1; return; }
63
+ // The repo's top, like every other yad command that commits — except the ledger door: a Product can be a
64
+ // subfolder of its repo, and the hook's refusal sends the person to `--manual` from there. Only there,
65
+ // because `--manual` also needs a staged file that hook refuses, so it cannot commit into a parent repo
66
+ // by accident; and `yad ship` (whose open-pr step needs the top) refuses `--manual`.
67
+ const inRepo = exists(path.join(root, '.git')) || (opts.manual && run('git', ['rev-parse', '--show-toplevel'], { cwd: root }).ok);
68
+ if (!inRepo) { fail('not a git repo'); process.exitCode = 1; return; }
69
+ // --manual and --reason come as a pair: an override with no reason records nothing, and a reason with
70
+ // no --manual would be silently dropped.
71
+ if (opts.manual && opts.reason === undefined) { fail('--manual needs --reason "<why>" — the reason is recorded in the commit as a Ledger-Override trailer'); process.exitCode = 1; return; }
72
+ if (!opts.manual && opts.reason !== undefined) { fail('--reason is only for --manual (the ledger override)'); process.exitCode = 1; return; }
51
73
 
52
74
  const staged = run('git', ['diff', '--cached', '--name-only'], { cwd: root }).stdout.split('\n').filter(Boolean);
53
75
  if (!staged.length) { fail('nothing staged — `git add` your atomic change first'); process.exitCode = 1; return; }
54
76
 
77
+ // An override is recorded only when there is something to override: a staged file the Product's
78
+ // ledger hook refuses. In a code repo, on a local ledger, or for a new epic's seed, nothing is.
79
+ // Asked without the skip switch: a YAD_HOOK_DISABLE left set in the shell would hide every hit.
80
+ const hookEnv = { ...process.env };
81
+ delete hookEnv.YAD_HOOK_DISABLE;
82
+ if (opts.manual && !stagedLedgerHits({ cwd: root, env: hookEnv }).hits.length) {
83
+ fail('--manual: nothing staged here is a file the ledger hook refuses (it guards the CI-owned ledger on a verified Product) — commit without --manual and --reason');
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+
55
88
  if (staged.length > ATOMIC_FILE_LIMIT && !opts.force) {
56
89
  fail(`${staged.length} files staged (atomic guard: ≤${ATOMIC_FILE_LIMIT}). Split the change, or pass --force.`);
57
90
  for (const f of staged) info(f);
@@ -64,8 +97,8 @@ export async function runCommit(root, opts = {}) {
64
97
  if (!task) {
65
98
  // spec-link is a code-repo gate (REPO_WIRING.common), not a Product gate — so a missing Task trailer
66
99
  // is expected on a Product PR (Shape artifact review or Product tooling) and only matters on a repo.
67
- const onHub = exists(productConfigPath(root));
68
- warn(onHub
100
+ const onProduct = exists(productConfigPath(root));
101
+ warn(onProduct
69
102
  ? 'no Task trailer (none given and branch has no -S0N-T0N) — fine for a Product PR; required on code-repo tasks (spec-link gate)'
70
103
  : 'no Task trailer (none given and branch has no -S0N-T0N) — spec-link gate will fail on a code repo');
71
104
  }
@@ -75,17 +108,20 @@ export async function runCommit(root, opts = {}) {
75
108
  message = buildCommitMessage({
76
109
  type: opts.type, subject: opts.message, task,
77
110
  contractChange: !!opts.contractChange, ai: opts.ai || 'none',
111
+ override: opts.manual ? String(opts.reason) : null,
78
112
  });
79
113
  } catch (e) { fail(e.message); process.exitCode = 1; return; }
80
114
 
81
115
  // The --json answer (E1): what was (or would be) committed.
82
- const answer = { message, task: task || null, files: staged, contractChange: !!opts.contractChange };
116
+ const answer = { message, task: task || null, files: staged, contractChange: !!opts.contractChange, manual: !!opts.manual, reason: opts.manual ? String(opts.reason).trim() : null };
83
117
  if (opts.dryRun) { log('\n' + c.dim(message) + '\n'); info('dry run — not committed'); return { ...answer, committed: false, dryRun: true }; }
84
118
 
85
- const r = run('git', ['commit', '-m', message], { cwd: root });
119
+ // Only yad's own hook is skipped — any other pre-commit hook (husky, lint-staged) still runs.
120
+ const r = run('git', ['commit', '-m', message], { cwd: root, ...(opts.manual ? { env: OWNING_COMMIT_ENV() } : {}) });
86
121
  if (!r.ok) { fail(`git commit failed — ${r.stderr.split('\n')[0] || r.code}`); process.exitCode = 1; return { ...answer, committed: false, dryRun: false }; }
87
122
  ok(`committed ${staged.length} file(s)${task ? ` for ${task}` : ''}`);
88
123
  if (opts.contractChange) warn('Contract-Change: yes — this routes back to the architecture gate');
124
+ if (opts.manual) warn('Ledger-Override recorded — the ledger-guard check on the pull request still fails this commit; the reason is there for the reviewer');
89
125
  const sha = run('git', ['rev-parse', 'HEAD'], { cwd: root });
90
126
  return { ...answer, committed: true, dryRun: false, commit: sha.ok ? sha.stdout : null };
91
127
  }
package/cli/docs.mjs CHANGED
@@ -6,12 +6,12 @@
6
6
  // and the manifest-hash staleness check (reusing the head-sha idea from repo.mjs). It NEVER touches
7
7
  // epic state, approvals, or the contract lock — docs are an output enrichment, not a gate.
8
8
  //
9
- // Pure mapping fns (deployTargetFromHub / siteBasePath / docsArtifactHash / docsStale / pagesWorkflow)
9
+ // Pure mapping fns (deployTargetFromProduct / siteBasePath / docsArtifactHash / docsStale / pagesWorkflow)
10
10
  // are exported for unit tests; the side-effecting runDocs orchestrates them.
11
11
  import path from 'node:path';
12
12
  import fs from 'node:fs';
13
13
  import { createHash } from 'node:crypto';
14
- import { c, log, ok, info, warn, hand, fail, readJSON, run, has, exists } from './lib.mjs';
14
+ import { c, log, ok, info, warn, hand, fail, readJSON, runLauncher, has, exists } from './lib.mjs';
15
15
  import { PROJECT_FILES } from './manifest.mjs';
16
16
  import { detectPlatform, platformReady } from './platform.mjs';
17
17
  import { gitHead } from './setup.mjs';
@@ -35,8 +35,8 @@ export function manifestPath(root, { epic, overview } = {}) {
35
35
 
36
36
  // ---- pure: platform/target + base path ----------------------------------------------------------
37
37
  // hub.json platform -> the default Pages target (github-pages | gitlab-pages | none/build-only).
38
- export function deployTargetFromHub(hub = {}) {
39
- const platform = hub?.platform || detectPlatform(hub?.git_url || '');
38
+ export function deployTargetFromProduct(productConfig = {}) {
39
+ const platform = productConfig?.platform || detectPlatform(productConfig?.git_url || '');
40
40
  if (platform === 'github') return 'github-pages';
41
41
  if (platform === 'gitlab') return 'gitlab-pages';
42
42
  return 'none';
@@ -175,31 +175,33 @@ on:
175
175
  workflow_dispatch:
176
176
  permissions:
177
177
  contents: read
178
- pages: write
179
- id-token: write
180
178
  concurrency:
181
179
  group: yad-docs-pages
182
180
  cancel-in-progress: true
183
181
  jobs:
184
182
  build-deploy:
185
183
  runs-on: ubuntu-latest
184
+ permissions:
185
+ contents: read
186
+ pages: write # publish the site
187
+ id-token: write # prove to Pages that this run is the deploy
186
188
  environment:
187
189
  name: github-pages
188
190
  url: \${{ steps.deployment.outputs.page_url }}
189
191
  steps:
190
- - uses: actions/checkout@v7
191
- - uses: actions/setup-node@v6
192
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
193
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
192
194
  with:
193
195
  node-version: 20
194
196
  - name: Build the overview + per-epic sites into ./public
195
197
  run: |
196
198
  ${BUILD_PUBLIC.map((l) => ` ${l}`).join('\n')}
197
- - uses: actions/configure-pages@v6
198
- - uses: actions/upload-pages-artifact@v5
199
+ - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
200
+ - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
199
201
  with:
200
202
  path: public
201
203
  - id: deployment
202
- uses: actions/deploy-pages@v5
204
+ uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
203
205
  `;
204
206
  }
205
207
  export function pagesWorkflowPath(platform) {
@@ -233,8 +235,8 @@ function buildSite(root, t, { npmOptional = false } = {}) {
233
235
  const install = exists(path.join(dir, 'package-lock.json')) ? ['ci'] : ['install'];
234
236
  const again = 'fix the error npm printed above, then run the command again';
235
237
  log(` ${c.dim('$')} npm ${install[0]} ${c.dim(`(${rel})`)}`);
236
- if (!run('npm', install, { cwd: dir, stdio: 'inherit' }).ok) return failed(`${site}: npm ${install[0]} failed`, again);
237
- if (!run('npm', ['run', 'build'], { cwd: dir, stdio: 'inherit' }).ok) return failed(`${site}: npm run build failed`, again);
238
+ if (!runLauncher('npm', install, { cwd: dir, stdio: 'inherit' }).ok) return failed(`${site}: npm ${install[0]} failed`, again);
239
+ if (!runLauncher('npm', ['run', 'build'], { cwd: dir, stdio: 'inherit' }).ok) return failed(`${site}: npm run build failed`, again);
238
240
  ok(`built ${site} ${c.dim('→ ' + path.relative(root, path.join(dir, 'dist')))}`);
239
241
  return { site, built: true, error: null };
240
242
  }