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/openpr.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  import path from 'node:path';
6
6
  import fs from 'node:fs';
7
7
  import { c, log, ok, info, warn, hand, fail, run, exists, readJSON } from './lib.mjs';
8
- import { PROJECT_FILES , productConfigPath } from './manifest.mjs';
8
+ import { PROJECT_FILES , productConfigPath, settingsEditHint } from './manifest.mjs';
9
9
  import {
10
10
  detectPlatform, createPr, platformLogin, resolveBaseBranch,
11
11
  } from './platform.mjs';
@@ -17,35 +17,43 @@ import { baseChangeLevel, changedSince, recentAuthorsFor, suggestedAuthorsFor }
17
17
  import { personLabel } from './riskmap.mjs';
18
18
  import { gateOpen } from './gate.mjs';
19
19
  import { unfoldedPaths, authorStepOf } from './fold.mjs';
20
+ import { registryEntryFor } from './find-product.mjs';
20
21
 
21
- // Resolve the target code repo: --repo <name> from the registry, else --dir, else cwd.
22
- function resolveRepo(root, { repo, dir }) {
22
+ // Resolve the target code repo: --repo <name> from the registry, else --dir, else cwd. `product` (E80) is
23
+ // the Product found from a code repo beside it: the registry is read there, and a repo run without
24
+ // --repo is looked up in it by its path — so its platform and default branch are the recorded ones.
25
+ function resolveRepo(root, { repo, dir, product }) {
26
+ const productRoot = product || root;
23
27
  if (repo) {
24
- const reg = readJSON(path.join(root, PROJECT_FILES.reposRegistry), { repos: [] });
25
- const found = reg.repos.find((r) => r.name === repo);
26
- if (found) return { repoRoot: path.resolve(root, found.path), meta: found };
28
+ const reg = readJSON(path.join(productRoot, PROJECT_FILES.reposRegistry), { repos: [] });
29
+ const found = (reg.repos || []).find((r) => r.name === repo);
30
+ if (found) return { repoRoot: path.resolve(productRoot, found.path), meta: found };
27
31
  }
28
- return { repoRoot: path.resolve(root, dir || '.'), meta: null };
32
+ const here = path.resolve(root, dir || '.');
33
+ // The checkout it runs in, when that checkout is a registered repo: worked at its top, from any
34
+ // folder inside it. Anything else (a worktree, a nested repo) stays exactly where it is.
35
+ const hit = registryEntryFor(product, here);
36
+ return { repoRoot: hit ? hit.top : here, meta: hit ? hit.meta : null };
29
37
  }
30
38
 
31
39
  // Which SDLC stage is this PR? The Product serves two vehicles; a code repo only one. Mirrors the
32
40
  // `--head` split the Product pattern gates (pr-title.sh/pr-template.sh) already apply:
33
41
  // code-repo — NOT the Product (a registry repo via --repo, or root is not a Product).
34
- // hub-shape — the Product itself AND head is a review/EP-* branch (artifact-review PR).
35
- // hub-tooling — the Product itself AND head is anything else (a tooling/CI change to the Product).
42
+ // product-shape — the Product itself AND head is a review/EP-* branch (artifact-review PR).
43
+ // product-tooling — the Product itself AND head is anything else (a tooling/CI change to the Product).
36
44
  // `meta` (truthy when resolved from the repos registry via --repo) is a connected code repo, so it is
37
45
  // never the Product regardless of its path. Otherwise "is the Product" = repoRoot resolves to root AND root
38
46
  // carries .sdlc/hub.json. path.resolve normalises `--dir .` / trailing slashes.
39
47
  export function detectStage(root, repoRoot, head, meta) {
40
48
  if (meta) return 'code-repo';
41
- const isHub = path.resolve(repoRoot) === path.resolve(root)
49
+ const isProduct = path.resolve(repoRoot) === path.resolve(root)
42
50
  && exists(productConfigPath(root));
43
- if (!isHub) return 'code-repo';
44
- return /^review\/EP-[a-z0-9-]+\//.test(head || '') ? 'hub-shape' : 'hub-tooling';
51
+ if (!isProduct) return 'code-repo';
52
+ return /^review\/EP-[a-z0-9-]+\//.test(head || '') ? 'product-shape' : 'product-tooling';
45
53
  }
46
54
 
47
55
  // The bundled code-task template — the same file `REPO_WIRING` installs into code repos, resolved
48
- // from the package (mirrors how manifest.mjs reads ../package.json). Used for a hub-tooling PR, whose
56
+ // from the package (mirrors how manifest.mjs reads ../package.json). Used for a product-tooling PR, whose
49
57
  // `.github/pull_request_template.md` is the ARTIFACT-REVIEW template (wrong shape for the code-task
50
58
  // Product gate). Falls back to a minimal body that still carries every section the gate requires.
51
59
  function codeTaskTemplate(platform) {
@@ -70,10 +78,10 @@ function codeTaskTemplate(platform) {
70
78
  }
71
79
 
72
80
  export function templateBody(repoRoot, platform, { task, summary, risk, contract, domains, stage }) {
73
- // hub-tooling: the Product's own template is artifact-review — use the bundled code-task template so the
81
+ // product-tooling: the Product's own template is artifact-review — use the bundled code-task template so the
74
82
  // body matches the shape the Product `pr-template` gate demands for a non-review head.
75
83
  let base;
76
- if (stage === 'hub-tooling') {
84
+ if (stage === 'product-tooling') {
77
85
  base = codeTaskTemplate(platform);
78
86
  } else {
79
87
  const tplPath = platform === 'gitlab'
@@ -241,11 +249,11 @@ export async function runOpenPr(root, opts = {}) {
241
249
  const branch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoRoot }).stdout;
242
250
  const stage = detectStage(root, repoRoot, branch, meta);
243
251
 
244
- // hub-shape: this is a Shape artifact-review PR (review/EP-*/<artifact> head on the Product). The
252
+ // product-shape: this is a Shape artifact-review PR (review/EP-*/<artifact> head on the Product). The
245
253
  // artifact-review title, body, and ledger bookkeeping all live in `yad gate open` — delegate to it
246
254
  // rather than emit the code-task shape (which the Product gate would reject). Push first (gateOpen does
247
255
  // not push), then hand off; any --title/--message is dropped (gateOpen sets `review: …`).
248
- if (stage === 'hub-shape') {
256
+ if (stage === 'product-shape') {
249
257
  const parsed = parseReviewBranch(branch);
250
258
  if (!parsed) { fail(`could not parse review branch '${branch}' (expected review/EP-<slug>/<artifact>)`); process.exitCode = 1; return; }
251
259
  // E44: the review carries what was FOLDED. A step file changed since then is not on this branch.
@@ -268,17 +276,17 @@ export async function runOpenPr(root, opts = {}) {
268
276
  }
269
277
 
270
278
  // The Product's default_branch, which only applies when the PR targets the Product ITSELF
271
- // (a hub-tooling branch) — for a connected code repo the Product's trunk belongs to a different repo and
272
- // must never leak in. Resolved AFTER the hub-shape hand-off above, which delegates its own base to
279
+ // (a product-tooling branch) — for a connected code repo the Product's trunk belongs to a different repo and
280
+ // must never leak in. Resolved AFTER the product-shape hand-off above, which delegates its own base to
273
281
  // `yad gate open`: resolving before it would spend a platform round-trip and print a base that the
274
282
  // delegated path then ignores.
275
- const hub = readJSON(productConfigPath(root), {});
283
+ const productConfig = readJSON(productConfigPath(root), {});
276
284
 
277
285
  // Resolve the base rather than assume it (#168). Hardcoding 'main' mis-based every PR on a repo
278
286
  // whose trunk is something else — and CodeRabbit decides auto-review eligibility from the base at
279
287
  // PR-OPEN time, so those PRs silently got no AI first pass at all.
280
288
  const { base: baseBranch, source: baseSource, platformDefault } = resolveBaseBranch(platform, {
281
- cwd: repoRoot, explicit: opts.base, meta, hub: stage === 'code-repo' ? null : hub, runner: opts.runner,
289
+ cwd: repoRoot, explicit: opts.base, meta, productConfig: stage === 'code-repo' ? null : productConfig, runner: opts.runner,
282
290
  });
283
291
  if (branch === baseBranch) { fail(`on ${baseBranch} — switch to your task branch first`); process.exitCode = 1; return; }
284
292
  info(`base ${baseBranch} ${c.dim(`(from ${baseSource})`)}`);
@@ -288,8 +296,8 @@ export async function runOpenPr(root, opts = {}) {
288
296
  // would mean contradicting their own committed config on every PR, forever.
289
297
  if (platformDefault && platformDefault !== baseBranch) {
290
298
  warn(`base '${baseBranch}' is not the repo default '${platformDefault}' — CodeRabbit skips auto-review on a non-default base unless .coderabbit.yaml lists it under reviews.base_branches, and retargeting later does NOT undo the skip`);
291
- if (baseSource === 'registry' || baseSource === 'hub') {
292
- hand(`the configured default_branch (${baseSource === 'hub' ? '.sdlc/hub.json' : '.sdlc/repos.json'}) disagrees with the platform — reconcile them, or allow '${baseBranch}' in .coderabbit.yaml`);
299
+ if (baseSource === 'registry' || baseSource === 'product') {
300
+ hand(`the configured default_branch (${baseSource === 'product' ? settingsEditHint(root) : '.sdlc/repos.json'}) disagrees with the platform — reconcile them, or allow '${baseBranch}' in .coderabbit.yaml`);
293
301
  } else {
294
302
  hand(`open against '${platformDefault}' (or pass --base ${platformDefault}) unless you meant to stack this PR`);
295
303
  }
@@ -342,7 +350,7 @@ export async function runOpenPr(root, opts = {}) {
342
350
  // feeding the number E72 caps with. `null` is the honest answer there: we are not standing in a
343
351
  // Product, so we did not count.
344
352
  const active = (() => {
345
- // Only the `code-repo` stage prints it (`routeCount`, below). On a `hub-tooling` PR this would
353
+ // Only the `code-repo` stage prints it (`routeCount`, below). On a `product-tooling` PR this would
346
354
  // otherwise walk the Product and every connected repo's history and throw the answer away.
347
355
  if (stage !== 'code-repo') return null;
348
356
  if (!exists(productConfigPath(root))) return null;
package/cli/people.mjs CHANGED
@@ -34,7 +34,8 @@ import fs from 'node:fs';
34
34
  import path from 'node:path';
35
35
  import { spawnSync } from 'node:child_process';
36
36
 
37
- import { c, info, readJSONStrict, warn } from './lib.mjs';
37
+ import { c, info, readJSONStrict, warn, forTerminal, asArg } from './lib.mjs';
38
+ import { judgeRepo, runnable } from './workspace.mjs';
38
39
  import { PROJECT_FILES, epicFiles } from './manifest.mjs';
39
40
  import { epicIds, epicRoot, unlistedLedgerDirs, FOUNDATION_EPIC, FOUNDATION_DIR, ledgerPersonLogin, legacyLogins, capLimit, capSeat, smallestTeam } from './epic-state.mjs';
40
41
  import { corruptShards, readShips } from './ledger.mjs';
@@ -312,10 +313,12 @@ export function gitHas(repoRoot, sha) {
312
313
  return r.status === 0;
313
314
  }
314
315
 
315
- function gitAuthors(repoRoot, since) {
316
- const git = (args) => spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 1 << 30 });
316
+ function gitAuthors(dir, since) {
317
+ const git = (args) => spawnSync('git', args, { cwd: dir, encoding: 'utf8', maxBuffer: 1 << 30 });
318
+ // Every reason below is printed: the folder is built from the shared registry (E81 review 15).
319
+ const repoRoot = forTerminal(dir);
317
320
  if (since === null) return { unknown: 'the window has no readable start date' };
318
- if (!fs.existsSync(repoRoot)) return { unknown: `${repoRoot} is not on disk` };
321
+ if (!fs.existsSync(dir)) return { unknown: `${repoRoot} is not on disk` };
319
322
  if (git(['rev-parse', '--is-inside-work-tree']).status !== 0) return { unknown: `${repoRoot} is not a git repo` };
320
323
  // A shallow clone holds only the newest commits. E67's rule, and the reason is even stronger here:
321
324
  // reading a truncated history as "these are all the people" is exactly the quiet under-count.
@@ -374,8 +377,17 @@ function connectedRepos(root) {
374
377
  if (!r || typeof r.name !== 'string' || !r.name) return { unknown: `${PROJECT_FILES.reposRegistry} holds a repo with no name` };
375
378
  // A repo with no path is registered but not on this machine. It is NOT "zero people": we cannot
376
379
  // read it, so the count is unknown rather than smaller.
377
- if (typeof r.path !== 'string' || !r.path) return { unknown: `repo '${r.name}' has no local path — its history cannot be counted here` };
378
- repos.push({ name: r.name, root: path.resolve(root, r.path), syncedHead: typeof r.syncedHead === 'string' ? r.syncedHead : null });
380
+ if (typeof r.path !== 'string' || !r.path) return { unknown: `repo '${forTerminal(r.name)}' has no local path — its history cannot be counted here` };
381
+ // Judged before git runs (E81 review 15), as every command that runs git in a registered path is: git
382
+ // run in a folder the Product commits, shaped as a repo, reads its `config`, which can run a
383
+ // command (`gpg.program` with `log.showSignature`). A refused entry is not "zero people" either —
384
+ // it is an unknown, said with the reason, never a silent skip.
385
+ const name = forTerminal(r.name);
386
+ const j = judgeRepo(root, r);
387
+ if (!runnable(j)) return { unknown: `repo '${name}': ${j.state === 'missing' ? 'not cloned on this machine' : j.linked ? 'reached through a link inside a repo\'s tree' : j.reason} — its history cannot be counted here` };
388
+ // A commit id only: `git cat-file -e <syncedHead>^{commit}` must never see anything else.
389
+ const syncedHead = typeof r.syncedHead === 'string' && /^[0-9a-f]{40,64}$/i.test(r.syncedHead) ? r.syncedHead : null;
390
+ repos.push({ name, arg: asArg(r.name), root: path.resolve(root, r.path), syncedHead });
379
391
  }
380
392
  return { repos };
381
393
  }
@@ -406,7 +418,7 @@ export function peopleEvidence(root, { today = todayString(), aliases = new Map(
406
418
  // error rather than throwing, so `gitHas` would answer false for a repo that is simply not on
407
419
  // this machine and send the reader off to fix the wrong thing.
408
420
  if (fs.existsSync(repo.root) && repo.syncedHead && !gitHas(repo.root, repo.syncedHead)) {
409
- unknown.push(`repo '${repo.name}': this clone does not hold the commit the registry last packed — it is behind, and a behind clone shows fewer people; fetch it, or re-pack it with \`yad repo refresh ${repo.name}\``);
421
+ unknown.push(`repo '${repo.name}': this clone does not hold the commit the registry last packed — it is behind, and a behind clone shows fewer people; fetch it, or re-pack it with \`yad repo refresh ${repo.arg}\``);
410
422
  continue;
411
423
  }
412
424
  const got = gitAuthors(repo.root, since);
@@ -611,10 +623,10 @@ export function teamHint(counted) {
611
623
  // The ONE wiring every surface uses (`yad mode`, `gate status`, `yad next`, `yad doctor`): null outside
612
624
  // solo mode, where no count is read; otherwise `teamHint` over the count the caller already read, or a
613
625
  // fresh one with the roster aliases (without them an older roster-shaped approval reads as a second
614
- // person). `solo` is the caller's `isSolo(hub)` — it lives in cli/gate.mjs, which imports this file.
615
- export function soloTeamHint(root, hub, { solo, headCount = null, today = null } = {}) {
626
+ // person). `solo` is the caller's `isSolo(productConfig)` — it lives in cli/gate.mjs, which imports this file.
627
+ export function soloTeamHint(root, productConfig, { solo, headCount = null, today = null } = {}) {
616
628
  if (!solo) return null;
617
- return teamHint(headCount || activePeople(root, { today: today || undefined, aliases: legacyLogins(hub) }));
629
+ return teamHint(headCount || activePeople(root, { today: today || undefined, aliases: legacyLogins(productConfig) }));
618
630
  }
619
631
 
620
632
  // The ONE way a suggestion is printed on a text surface: a known one as a warning line (`! …`), an