yadflow 4.3.0 → 4.5.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 (48) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +4 -2
  3. package/bin/commands.mjs +3 -1
  4. package/bin/yad.mjs +80 -14
  5. package/cli/claims.mjs +16 -13
  6. package/cli/detect.mjs +2 -2
  7. package/cli/docs.mjs +1 -1
  8. package/cli/doctor.mjs +108 -3
  9. package/cli/epic-state.mjs +121 -24
  10. package/cli/epic.mjs +7 -5
  11. package/cli/manifest.mjs +3 -0
  12. package/cli/members.mjs +822 -0
  13. package/cli/next.mjs +21 -9
  14. package/cli/people.mjs +13 -3
  15. package/cli/platform.mjs +1 -1
  16. package/cli/pr-list.mjs +125 -0
  17. package/cli/protection.mjs +4 -4
  18. package/cli/recommend.mjs +301 -0
  19. package/cli/setup.mjs +3 -2
  20. package/cli/skill.mjs +263 -63
  21. package/cli/standup.mjs +709 -0
  22. package/cli/toolbox.mjs +433 -11
  23. package/cli/usage.mjs +18 -11
  24. package/cli/workspace.mjs +14 -5
  25. package/package.json +5 -3
  26. package/skills/sdlc/config.yaml +1 -1
  27. package/skills/sdlc/module-help.csv +1 -1
  28. package/skills/yad-backfill/SKILL.md +40 -7
  29. package/skills/yad-backfill/references/backfill.md +5 -4
  30. package/skills/yad-change/SKILL.md +1 -1
  31. package/skills/yad-checks/templates/checks/member-check.mjs +197 -0
  32. package/skills/yad-checks/templates/github/yad-product-checks.yml +15 -0
  33. package/skills/yad-checks/templates/gitlab/yad-product-checks.gitlab-ci.yml +9 -0
  34. package/skills/yad-connect-design/SKILL.md +16 -0
  35. package/skills/yad-connect-learning/SKILL.md +14 -0
  36. package/skills/yad-connect-repos/SKILL.md +34 -5
  37. package/skills/yad-connect-repos/references/code-context.md +5 -5
  38. package/skills/yad-connect-testing/SKILL.md +20 -0
  39. package/skills/yad-docs/SKILL.md +1 -1
  40. package/skills/yad-epic/references/state-schema.md +10 -2
  41. package/skills/yad-learn/SKILL.md +14 -0
  42. package/skills/yad-run/SKILL.md +7 -4
  43. package/skills/yad-run/references/run-loop.md +2 -1
  44. package/skills/yad-spec/SKILL.md +37 -6
  45. package/skills/yad-spec/references/spec-handoff.md +2 -2
  46. package/skills/yad-status/SKILL.md +3 -0
  47. package/skills/yad-test-cases/SKILL.md +20 -0
  48. package/skills/yad-ui/SKILL.md +36 -3
package/CHANGELOG.md CHANGED
@@ -1,3 +1,66 @@
1
+ # [4.5.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.4.0...v4.5.0) (2026-10-06)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **member:** E131 review 1 — links, twins, proof and pushes ([1179a97](https://github.com/abdelrahmannasr/yadflow/commit/1179a9731e4ff9f8d6b80f5aed82d5809bde5ed2))
7
+ * **member:** E131 review 2 — gate-live trust, account ids, clean-ups ([74d41f6](https://github.com/abdelrahmannasr/yadflow/commit/74d41f65a585fdf06639be87688f69353eb61916))
8
+ * **member:** E131 review 3 — trust only files the gate judged ([2f9af14](https://github.com/abdelrahmannasr/yadflow/commit/2f9af141e524ca7daacbce8c2799f56c72c6d75e))
9
+ * **member:** E131 review 4 — trust is read from origin's default branch ([43af9c4](https://github.com/abdelrahmannasr/yadflow/commit/43af9c4565c3bbfae7042d7765df62150fa64447))
10
+ * **member:** E131 review 5 — every change re-proves every email ([041937c](https://github.com/abdelrahmannasr/yadflow/commit/041937c936f5d9f29683ef6f9d4cc6fb93ad83bd))
11
+ * **member:** E131 review 6 — re-runs, origin/HEAD, partial lists ([e9bad00](https://github.com/abdelrahmannasr/yadflow/commit/e9bad002eff766705d193b1515baa3d39ae0c360))
12
+ * **standup:** E132 review 1 — gate rule, malformed ledgers, terminal safety ([a0e7d47](https://github.com/abdelrahmannasr/yadflow/commit/a0e7d4750e7b485582cded4b55dfcc212013a101))
13
+ * **standup:** E132 review 2 — missed twins of round 1 ([ba1ce61](https://github.com/abdelrahmannasr/yadflow/commit/ba1ce61b961decbea46298b714fd33f811fb264a))
14
+ * **standup:** E132 review 3 — paths, disputed accounts, left members ([add86cc](https://github.com/abdelrahmannasr/yadflow/commit/add86cca821ab29afa79838734a3b83621dd2330))
15
+ * **standup:** E132 review 4 — Windows paths, a name is plain text ([435225f](https://github.com/abdelrahmannasr/yadflow/commit/435225f48a21fe8a850ba65c0c7f1315df6bbb1e))
16
+ * **standup:** E132 review 5 — path names in square brackets ([7ac3cb2](https://github.com/abdelrahmannasr/yadflow/commit/7ac3cb265c309f0e707e6dae32076796a5e2c240))
17
+
18
+
19
+ ### Features
20
+
21
+ * **join:** E131 join ends with yad member add ([3295429](https://github.com/abdelrahmannasr/yadflow/commit/3295429092b549707a03eecacddc50d15d1df77a))
22
+ * **member:** E131 team member files, proven by the platform ([3e60bb6](https://github.com/abdelrahmannasr/yadflow/commit/3e60bb62bb2edb467be4d5f8562b9fe1bac6ca0a))
23
+ * **standup:** yad standup — a daily status per team member (E132) ([00f32ce](https://github.com/abdelrahmannasr/yadflow/commit/00f32ce742166e8b82e796b3eef85b379edd4cee))
24
+
25
+ # [4.4.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.3.0...v4.4.0) (2026-10-03)
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * **skill:** E51 review 1 — read the route the epic records, never a guess ([cefdb21](https://github.com/abdelrahmannasr/yadflow/commit/cefdb216139e4608961b83a5ec61f1ca6cb318c7))
31
+ * **skill:** E51 review 2 — say an epic's route is unknown; simplify the change-epic seed ([c5e6f0e](https://github.com/abdelrahmannasr/yadflow/commit/c5e6f0e12e56a768196088aff10d2eebd3f70293))
32
+ * **skill:** E52 flag a pick bound in place of yadflow's skill; list prints the catalogue's age ([a0b76f3](https://github.com/abdelrahmannasr/yadflow/commit/a0b76f30fd1d9fb3123977532ab0ef055daf89bf))
33
+ * **skill:** E52 review 1 — yadflow's skill must run last, plugin names, route view in list ([fcf675a](https://github.com/abdelrahmannasr/yadflow/commit/fcf675a6fe64788f3d0b2c12f8f7aad14eb6448c))
34
+ * **skill:** E52 review 2 — a bare bound name never matches a plugin-qualified pick ([4199fd3](https://github.com/abdelrahmannasr/yadflow/commit/4199fd332f36eb532f9aa4bd2a4f05a77bfa6cec))
35
+ * **skill:** E52 review 3 — a marketplace-spelled binding is the same pick ([04d7aaa](https://github.com/abdelrahmannasr/yadflow/commit/04d7aaafe640253dd0ecc48e922c993e0d7b3860))
36
+ * **toolbox:** E87 review 1 — CRLF, unseen tools, no Product, honest Repomix fallback ([dc3bd75](https://github.com/abdelrahmannasr/yadflow/commit/dc3bd7577271c5fb71fe5522e8a6d1f2c1fff575))
37
+ * **toolbox:** E87 review 10 — unclosed fence refused, frontmatter skipped ([577786c](https://github.com/abdelrahmannasr/yadflow/commit/577786c2ff939be1bf7565c9bc4f3b72d9569cd7))
38
+ * **toolbox:** E87 review 11 notes — frontmatter hides nothing, exact round trip ([b8bb924](https://github.com/abdelrahmannasr/yadflow/commit/b8bb924c5320f3fdf531f03fa968f77dc970f8c8))
39
+ * **toolbox:** E87 review 12 — a section can never cross the frontmatter ([cb1c111](https://github.com/abdelrahmannasr/yadflow/commit/cb1c11184fd2caea1e2f6b4c7082b65e5de462f3))
40
+ * **toolbox:** E87 review 13 notes — frontmatter closes at '--- ' or '...' ([df6c644](https://github.com/abdelrahmannasr/yadflow/commit/df6c644f639eaeef04b36eb1da2116b472452f39))
41
+ * **toolbox:** E87 review 14 — frontmatter needs a YAML key; no write leaves two copies ([20f3c75](https://github.com/abdelrahmannasr/yadflow/commit/20f3c75a231461d42566474d8be972e08edc59df))
42
+ * **toolbox:** E87 review 15 — check names the two-copy refusal too ([88757ac](https://github.com/abdelrahmannasr/yadflow/commit/88757acf9015dc8a8b2adae7137fdf1b8a151959))
43
+ * **toolbox:** E87 review 2 — connector-only sections, safer section finder ([00127ec](https://github.com/abdelrahmannasr/yadflow/commit/00127ecd4ae61c10cc5b34beaf32d42512d8ea05))
44
+ * **toolbox:** E87 review 3 — a removal works, a misread is refused by content ([a42bc78](https://github.com/abdelrahmannasr/yadflow/commit/a42bc78bc5a53ba1e9c17073615f9313bab3b66e))
45
+ * **toolbox:** E87 review 4 — one heading rule, a guard that quotes its line ([768b43a](https://github.com/abdelrahmannasr/yadflow/commit/768b43a7f8df84aeb1ca1d53818a421a1dca5ba2)), closes [#s](https://github.com/abdelrahmannasr/yadflow/issues/s)
46
+ * **toolbox:** E87 review 5 notes — fence-aware copy count, dividers, linear heading read ([d9eb679](https://github.com/abdelrahmannasr/yadflow/commit/d9eb6793ead27dfa9f76f60bc301a45df727a832)), closes [#s](https://github.com/abdelrahmannasr/yadflow/issues/s)
47
+ * **toolbox:** E87 review 6 — truly linear heading read, underline guard reads real headings ([83d0885](https://github.com/abdelrahmannasr/yadflow/commit/83d0885c9f8c6c216a35e740b365c6eebaca6b54)), closes [#s](https://github.com/abdelrahmannasr/yadflow/issues/s) [#hashtag](https://github.com/abdelrahmannasr/yadflow/issues/hashtag)
48
+ * **toolbox:** E87 review 7 — no slow pattern left in the section readers ([7201076](https://github.com/abdelrahmannasr/yadflow/commit/72010765951c569ea0c0cbc52b67c13f96c9f248))
49
+ * **toolbox:** E87 review 8 — a divider ends the block; the guard is checked against marked ([750187c](https://github.com/abdelrahmannasr/yadflow/commit/750187cecd34e8ff49c39e141424656781def287)), closes [#tag](https://github.com/abdelrahmannasr/yadflow/issues/tag)
50
+ * **toolbox:** E88 review 1 — refuse future and impossible vetting dates; pin every Repomix line ([20048b5](https://github.com/abdelrahmannasr/yadflow/commit/20048b562129a3860a521dc8a159cc8641dc3d9a))
51
+ * **toolbox:** E88 review 2 — install-line test fails closed; pin test covers npm install ([e955cbd](https://github.com/abdelrahmannasr/yadflow/commit/e955cbddc5d1d596c833dfb596b1536516b4d489))
52
+ * **toolbox:** E88 review 3 — a terms page that says license is not a licence file ([34edaf8](https://github.com/abdelrahmannasr/yadflow/commit/34edaf84f0a4a3d7338de1aae61964f975c6e2e3))
53
+ * **toolbox:** E88 review 4 — tell a licence file by where it lives, not its letter case ([4e9dbc0](https://github.com/abdelrahmannasr/yadflow/commit/4e9dbc0850ca9cba27ceba00e8b9dea777c1d1e6))
54
+ * **toolbox:** E88 review 5 — a repository's front page is not a licence file ([3f9fe15](https://github.com/abdelrahmannasr/yadflow/commit/3f9fe15a54a259c7c784a18b4e52e69ce50a7104))
55
+
56
+
57
+ ### Features
58
+
59
+ * **skill:** E51 bind a skill for one route only ([654488c](https://github.com/abdelrahmannasr/yadflow/commit/654488c7e7d876fbe29122959f2befe1122ebffe))
60
+ * **skill:** E52 recommendation catalogue and yad skill recommend ([2b4fba0](https://github.com/abdelrahmannasr/yadflow/commit/2b4fba05a6e27af978dcaeb25fe61a0dc61c36b6))
61
+ * **toolbox:** E88 vet every toolbox tool, and pin Repomix to the vetted version ([e4026f3](https://github.com/abdelrahmannasr/yadflow/commit/e4026f3140428a6b80c1842e410c8174f5ecb205))
62
+ * **toolbox:** every skill that uses a tool declares its fallback (E87) ([8d4963e](https://github.com/abdelrahmannasr/yadflow/commit/8d4963e603e92be0fcbc616e98a5ea5939befbc4))
63
+
1
64
  # [4.3.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.2.0...v4.3.0) (2026-10-01)
2
65
 
3
66
 
package/README.md CHANGED
@@ -144,7 +144,7 @@ unless CI runs it), or `yad gate advance` with no platform, is the human decisio
144
144
  | --- | --- | --- |
145
145
  | Existing repositories | `yad init`, in the folder that holds them | Run the `yad-backfill` skill first. It records what already exists. |
146
146
  | One existing repository with all the code | `yad setup --brownfield --monorepo`, inside it | The same: run `yad-backfill` first. |
147
- | A team that already uses Yadflow | `yad join <Product clone URL>` | It clones the Product and every repository it lists. It sets up only this machine and never commits. |
147
+ | A team that already uses Yadflow | `yad join <Product clone URL>` | It clones the Product and every repository it lists and sets up this machine. Last, it records you as a team member (`yad member add`): your accounts and verified emails, on a branch of its own with its own pull request. |
148
148
 
149
149
  **Small changes.** Not every change needs the full route:
150
150
 
@@ -256,8 +256,10 @@ The record is worth trusting because people approve what goes into it.
256
256
  - the Shape and Build steps with review gates
257
257
  - linked changes and `yad thread`
258
258
  - `yad history`, `yad next` and `yad usage`
259
+ - `yad standup`, a daily status for each team member — what they did since the last working day, what they are on now, and what is waiting — rebuilt live from git, the ledgers and the platform, facts only
259
260
  - `yad detect`, which lists the skills, agents, MCP servers and plugins installed for each AI agent
260
- - `yad toolbox list`, which shows the external tools yadflow can use, which ones you have, and what happens without each; `yad toolbox add` / `remove` record the tools your team uses, and `yad toolbox check` lists the ones not ready on your machine (`yad setup`, `check`, `update` and `join` print the same list, and never install anything)
261
+ - `yad toolbox list`, which shows the external tools yadflow can use, which ones you have, and what happens without each; `yad toolbox add` / `remove` record the tools your team uses, and `yad toolbox check` lists the ones not ready on your machine (`yad setup`, `check`, `update` and `join` print the same list, and never install anything); each skill that uses a tool reads the same answer and says what it does without it
262
+ - `yad skill recommend`, which lists hand-picked skills from the recommended skill collections for each step, why each fits, and the command to bind one; it binds nothing, and every pick was checked against its source
261
263
  - draft saving, claims and owners
262
264
  - the CI checks
263
265
  - the Review Companion
package/bin/commands.mjs CHANGED
@@ -18,7 +18,7 @@ export { gateOpen, gateSync, gateComments, gateStatus, gateCi, gateReview, gateT
18
18
  export { gateApprove, gateComment, gateAdvance } from '../cli/gate-local.mjs';
19
19
  export { isValidEpicId, productDriftError, seedableProfiles } from '../cli/epic-state.mjs';
20
20
  export { runEpicNew, runFoundationNew, runFoundationStatus } from '../cli/epic.mjs';
21
- export { runSkillBind, runSkillList, runSkillUnbind } from '../cli/skill.mjs';
21
+ export { runSkillBind, runSkillList, runSkillRecommend, runSkillUnbind } from '../cli/skill.mjs';
22
22
  export { runCommit } from '../cli/commit.mjs';
23
23
  export { runOpenPr } from '../cli/openpr.mjs';
24
24
  export { reviewTrailer, reviewContext, reviewNudge, reviewReconcile, reviewWalkthrough } from '../cli/review.mjs';
@@ -29,6 +29,8 @@ export { runCapture } from '../cli/capture.mjs';
29
29
  export { runFold } from '../cli/fold.mjs';
30
30
  export { runClaims } from '../cli/claims.mjs';
31
31
  export { runAssign, runUnassign, runOwners } from '../cli/owners.mjs';
32
+ export { runMemberAdd, runMemberList, runMemberRemove } from '../cli/members.mjs';
33
+ export { runStandup, foreignFlags as standupForeignFlags } from '../cli/standup.mjs';
32
34
  export { runIndex } from '../cli/index-command.mjs';
33
35
  export { runHistory, HISTORY_FLAGS } from '../cli/history.mjs';
34
36
  export { runRepo } from '../cli/repo.mjs';
package/bin/yad.mjs CHANGED
@@ -21,7 +21,8 @@ ${c.bold('Start a workspace')} ${c.dim('(the folder holding product/ and the cod
21
21
  Clone the Product into [folder]/product/ (default: the URL's last name) and
22
22
  every repo it registers; a failed repo clone is reported, never fatal. Then
23
23
  only per-machine steps: git-ignored skill copies + the git pre-commit hook.
24
- Never commits, pushes, or changes a shared file
24
+ Last, \`yad member add\` (below): your member file, on a branch with its own
25
+ PR/MR — the only shared change join makes. A problem there never fails join
25
26
 
26
27
  ${c.bold('Setup & maintenance')}
27
28
  yad setup Guided first-run setup (profile interview, install, connect & wire repos,
@@ -68,6 +69,31 @@ ${c.bold('Setup & maintenance')}
68
69
  installs in this clone refuses a person's hand commit to the ledger.
69
70
  YAD_HOOK_DISABLE=1 skips.
70
71
 
72
+ ${c.bold('Team members')} ${c.dim('(identity only — no roles, no reviewers, no allowlist)')}
73
+ yad member add [--no-push] Prove who you are and write .sdlc/members/<platform>-<login>.json:
74
+ your account on the Product's platform (logs you in if needed),
75
+ your other GitHub/GitLab accounts and git emails that the platform
76
+ says are VERIFIED (stored hashed). One commit per email on
77
+ yad/member/<login>, pushed, with a PR/MR. Never your checkout
78
+ yad member list [--json] Every member: active (committed or approved within the TTL,
79
+ default 90 days), idle (quiet, still has access), left (the
80
+ platform says no access — off the team list) or unknown
81
+ yad member remove [<login>] [--reason <why>]
82
+ Delete a member file — your own, or anyone's with --reason
83
+
84
+ ${c.bold('Daily standup')} ${c.dim('(derived live, read-only, facts only — no score, no ranking)')}
85
+ yad standup Each member on the team list: done since the last standup
86
+ (commits, approvals, comments, closed steps, ships, merged
87
+ PRs/MRs), working on now (owned steps, open claims, drafts, open
88
+ PRs/MRs) and waiting (gates on their steps, their next move, their
89
+ PRs/MRs with failing checks or no approving review). Then the
90
+ people seen who are not on the list, and who has left.
91
+ Window: from midnight of the previous working day (Monday reads
92
+ back to Friday). Asks the platform about the Product and every
93
+ registered repo; one it cannot read says "platform not read"
94
+ flags: --since <YYYY-MM-DD>|<N>h|<N>d, --tz <zone> (default UTC),
95
+ --member <login> | --me, --format md|html, --out <file>, --json
96
+
71
97
  ${c.bold('Team usage (EM adoption & behavior report)')}
72
98
  yad usage Build a per-member report (HTML) from git + the SDLC ledgers
73
99
  (derived, read-only — writes no tracked state)
@@ -111,12 +137,22 @@ ${c.bold('Where am I / what next')}
111
137
  yad next --all Every active epic's next action at once
112
138
  yad next [<epic>] --json The same answer as a machine-readable action object (for
113
139
  agents/CI) — always every epic, so --all is implied
114
- yad skill list [--json] Which skill runs which step, and whether that is this
115
- project's choice or the engine's default
116
- yad skill bind <step> <skill> [<skill> ...]
117
- Bind a step to a skill of your own. Several skills run in
118
- the order given, one after another — each costs tokens
119
- yad skill unbind <step> Drop the binding; the step goes back to the engine's default
140
+ yad skill list [--profile <p> | --epic <id>] [--json]
141
+ Which skill runs which step, whether that is one route's
142
+ choice, the project's or the engine's default, and whether
143
+ each skill is installed here (--epic: that epic's route)
144
+ yad skill bind <step> <skill> [<skill> ...] [--profile <p>]
145
+ Bind a step to a skill of your own — for every epic, or
146
+ with --profile for epics on that route only (it wins).
147
+ Several skills run in the order given — each costs tokens.
148
+ Warns when the skill is not installed here
149
+ yad skill unbind <step> [--profile <p>]
150
+ Drop the binding; the step goes back to the project-wide
151
+ binding, then the engine's default
152
+ yad skill recommend [<step>] [--profile <p> | --epic <id>] [--json]
153
+ Hand-picked skills from the recommended pools for each step,
154
+ why each fits, whether it is installed here, and the bind
155
+ command (pool skill first, yadflow's skill last). Binds nothing
120
156
  yad toolbox list [--json] [--dir <folder>]
121
157
  The external tools yadflow can use — core (offered at setup),
122
158
  recommended pools, the design/testing/learning connectors and
@@ -339,7 +375,9 @@ ${c.bold('Environment')}
339
375
  YAD_NO_REPORT=1 Never offer to file a bug report after a failure
340
376
  YAD_PLATFORM_LOGIN=0 Name a record's author by git user.name; never ask gh/glab who is logged in`;
341
377
 
342
- const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--team', '--body', '--out', '--since', '--until', '--member', '--format', '--reason', '--profile', '--parent', '--inherits', '--to', '--retro-ship', '--merge-commit', '--path', '--ide-targets', '--theme', '--thread', '--by', '--count', '--engagement', '--keep', '--role', '--fallback', '--detect', '--install', '--source']);
378
+ const STANDUP_USAGE = 'usage: yad standup [--since <YYYY-MM-DD>|<N>h|<N>d] [--tz <zone>] [--member <login>|--me] [--format md|html] [--out <file>] [--json]';
379
+
380
+ const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--team', '--body', '--out', '--since', '--until', '--member', '--format', '--reason', '--profile', '--parent', '--inherits', '--to', '--retro-ship', '--merge-commit', '--path', '--ide-targets', '--theme', '--thread', '--by', '--count', '--engagement', '--keep', '--role', '--fallback', '--detect', '--install', '--source', '--tz']);
343
381
 
344
382
  function parseArgs(argv) {
345
383
  const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
@@ -350,6 +388,7 @@ function parseArgs(argv) {
350
388
  else if (a === '--force') o.force = true;
351
389
  else if (a === '--contract-change') o.contractChange = true;
352
390
  else if (a === '--manual') o.manual = true;
391
+ else if (a === '--me') o.me = true;
353
392
  else if (a === '--no-push') o.noPush = true;
354
393
  else if (a === '--no-fetch') o.noFetch = true;
355
394
  else if (a === '--push') o.push = true;
@@ -425,7 +464,7 @@ let runningCmd = null;
425
464
  const ACTIONS = {
426
465
  epic: { known: ['new'] },
427
466
  foundation: { known: ['new', 'status'] },
428
- skill: { known: ['list', 'bind', 'unbind'], default: 'list' },
467
+ skill: { known: ['list', 'bind', 'unbind', 'recommend'], default: 'list' },
429
468
  gate: { known: ['open', 'sync', 'comments', 'status', 'repair', 'review', 'walkthrough', 'trailer', 'ci', 'approve', 'comment', 'advance'] },
430
469
  review: { known: ['trailer', 'context', 'chat', 'cards', 'walkthrough', 'nudge', 'reconcile'] },
431
470
  tidy: { known: ['up'] },
@@ -437,6 +476,7 @@ const ACTIONS = {
437
476
  reconcile: { known: ['check', 'refresh', 'wire'] },
438
477
  hook: { known: ['ledger-guard'] },
439
478
  toolbox: { known: ['list', 'check', 'add', 'remove'], default: 'list' },
479
+ member: { known: ['add', 'list', 'remove'], default: 'list' },
440
480
  };
441
481
  // E80 — how each command finds its folder. A PRODUCT command works on the Product; a REPO command works
442
482
  // on the code repo it runs in and reads the Product beside it. The rest take their folder as given:
@@ -445,7 +485,7 @@ const PRODUCT_CMDS = new Set([
445
485
  'check', 'update', 'doctor', 'migrate', 'usage', 'sync-status', 'epic', 'foundation', 'skill', 'next',
446
486
  'skip', 'unskip', 'defer', 'undefer', 'dial', 'mode', 'kill', 'unkill', 'unblock', 'gate', 'checkpoint',
447
487
  'capture', 'claims', 'assign', 'unassign', 'owners', 'fold', 'tidy', 'index', 'history', 'repo',
448
- 'risk-map', 'codeowners', 'docs', 'thread', 'reconcile',
488
+ 'risk-map', 'codeowners', 'docs', 'thread', 'reconcile', 'member', 'standup',
449
489
  ]);
450
490
  const REPO_CMDS = new Set(['commit', 'open-pr', 'ship', 'review']);
451
491
  // The Product commands that WRITE its files without first checking it is one. Run from a code repo they
@@ -458,6 +498,7 @@ function writesProduct(cmd, o) {
458
498
  if (cmd === 'epic' || cmd === 'foundation') return action === 'new';
459
499
  if (cmd === 'skill') return action === 'bind' || action === 'unbind';
460
500
  if (cmd === 'dial') return o.to !== undefined;
501
+ if (cmd === 'member') return o._[1] === 'add' || o._[1] === 'remove';
461
502
  // `--wire` writes a CI workflow; `--refresh` only rebuilds sites the folder already holds.
462
503
  if (cmd === 'docs') return action === 'sync' && !!o.wire;
463
504
  return false;
@@ -735,11 +776,12 @@ async function main() {
735
776
  }
736
777
  case 'skill': {
737
778
  const [, action, step, ...rest] = o._;
738
- if (action === 'list' || action === undefined) { result = commands.runSkillList(o.dir, { json: o.json }); break; }
779
+ if (action === 'list' || action === undefined) { result = commands.runSkillList(o.dir, { json: o.json, profile: o.profile, epic: o.epic }); break; }
739
780
  if ((action === 'bind' || action === 'unbind') && noProduct()) break;
740
- if (action === 'bind') { result = commands.runSkillBind(o.dir, { step, skills: rest }); break; }
741
- if (action === 'unbind') { result = commands.runSkillUnbind(o.dir, { step }); break; }
742
- refuse(`unknown skill action: ${action} (list, bind, unbind)`, 'usage: yad skill list [--json] | yad skill bind <step> <skill> [<skill> ...] | yad skill unbind <step>');
781
+ if (action === 'bind') { result = commands.runSkillBind(o.dir, { step, skills: rest, profile: o.profile }); break; }
782
+ if (action === 'unbind') { result = commands.runSkillUnbind(o.dir, { step, profile: o.profile }); break; }
783
+ if (action === 'recommend') { result = commands.runSkillRecommend(o.dir, { step: step ?? null, json: o.json, profile: o.profile, epic: o.epic }); break; }
784
+ refuse(`unknown skill action: ${action} (list, bind, unbind, recommend)`, 'usage: yad skill list [--profile <p> | --epic <id>] [--json] | yad skill bind <step> <skill> [<skill> ...] [--profile <p>] | yad skill unbind <step> [--profile <p>] | yad skill recommend [<step>] [--profile <p> | --epic <id>] [--json]');
743
785
  break;
744
786
  }
745
787
  case 'next': {
@@ -876,6 +918,30 @@ async function main() {
876
918
  if (o.hook) process.exitCode = 0;
877
919
  break;
878
920
  }
921
+ case 'member': {
922
+ // E131. One file per person, identity only.
923
+ const [, action = 'list', who, extra] = o._;
924
+ const usage = 'usage: yad member add [--no-push] | yad member list [--json] | yad member remove [<login>] [--reason "<why>"]';
925
+ if (!['add', 'list', 'remove'].includes(action)) { refuse(`unknown member action: ${action} (add, list, remove)`, usage); break; }
926
+ if ((action !== 'remove' && who) || extra) { refuse(`unexpected word: ${extra || who}`, usage); break; }
927
+ if (noProduct()) break;
928
+ if (action === 'add') result = await commands.runMemberAdd(o.dir, { noPush: !!o.noPush, today });
929
+ else if (action === 'list') result = await commands.runMemberList(o.dir, { today });
930
+ else result = await commands.runMemberRemove(o.dir, { login: who ?? null, reason: o.reason ?? null });
931
+ break;
932
+ }
933
+ case 'standup': {
934
+ // E132. Read-only; every flag that is not standup's own is refused, as history refuses them.
935
+ const foreign = commands.standupForeignFlags(process.argv.slice(2));
936
+ if (foreign.length) { refuse(`yad standup does not take ${foreign.join(', ')}`, STANDUP_USAGE); break; }
937
+ if (o._[1]) { refuse(`yad standup takes no words (got: ${o._[1]})`, STANDUP_USAGE); break; }
938
+ if (noProduct()) break;
939
+ result = await commands.runStandup(o.dir, {
940
+ since: o.since ?? null, tz: o.tz ?? 'UTC', member: o.member ?? null, me: !!o.me,
941
+ format: o.format ?? null, out: o.out ?? null, json: !!o.json,
942
+ });
943
+ break;
944
+ }
879
945
  case 'claims': {
880
946
  // E46. Read from the capture branches; nothing is recorded here.
881
947
  const [, epic, extra] = o._;
package/cli/claims.mjs CHANGED
@@ -135,6 +135,21 @@ export function claimWarnings(root, changed, { env = process.env, now = Date.now
135
135
  }
136
136
  export const warningText = (hits, now) => `yad claims: ${hits.length === 1 ? 'this file is' : 'these files are'} also being edited by someone else — advice, not a lock; talk to them before you go further:\n${hits.map((c) => ` • ${claimLine(c, now)}`).join('\n')}`;
137
137
 
138
+ // Fetch everyone's capture branches and the default branch: 'done' | 'failed' | 'local' (no origin). One
139
+ // fetch, 30 seconds at most. `yad claims` and `yad standup` (E132) both read what it brings.
140
+ export function fetchCaptures(root, env = process.env) {
141
+ const git = gitIn(root, env);
142
+ if (!git(['remote', 'get-url', 'origin']).ok) return 'local';
143
+ const opts = { cwd: root, encoding: 'utf8', timeout: 30_000, env: { ...env, ...pushEnv(env) } };
144
+ const r = spawnSync('git', fetchAllArgs({ prune: true }), opts);
145
+ const fetched = r.status === 0 ? 'done' : 'failed';
146
+ // The default branch, for the "landed" rule — on its own: a name guessed wrong (no Product default, no
147
+ // origin/HEAD) must not stop the capture branches from arriving. Its failure changes nothing.
148
+ const def = defaultRef(root, git).name;
149
+ if (fetched === 'done') spawnSync('git', ['-c', 'http.lowSpeedLimit=1000', '-c', 'http.lowSpeedTime=20', 'fetch', '--quiet', '--no-tags', 'origin', `+refs/heads/${def}:refs/remotes/origin/${def}`], opts);
150
+ return fetched;
151
+ }
152
+
138
153
  // `yad claims [<epic>] [--no-fetch]`. Returns a plain object (E1 makes it the `--json` answer).
139
154
  export async function runClaims(root, { epic = null, noFetch = false, env = process.env, now = Date.now() } = {}) {
140
155
  const refuse = (msg, hint) => { fail(msg); if (hint) hand(hint); process.exitCode = 1; };
@@ -142,19 +157,7 @@ export async function runClaims(root, { epic = null, noFetch = false, env = proc
142
157
  if (!git(['rev-parse', '--show-toplevel']).ok) return refuse('not a git repository — there are no capture branches to read');
143
158
  if (!fs.existsSync(productConfigPath(root))) return refuse('not a Product (no .sdlc/hub.json or product.json here)', 'run it from the Product root, or pass --dir');
144
159
  if (epic && !/^EP-[a-z0-9-]+$/.test(epic)) return refuse(`not an epic id: ${epic}`);
145
- let fetched = 'skipped';
146
- if (!noFetch) {
147
- if (!git(['remote', 'get-url', 'origin']).ok) fetched = 'local';
148
- else {
149
- const opts = { cwd: root, encoding: 'utf8', timeout: 30_000, env: { ...env, ...pushEnv(env) } };
150
- const r = spawnSync('git', fetchAllArgs({ prune: true }), opts);
151
- fetched = r.status === 0 ? 'done' : 'failed';
152
- // The default branch, for the "landed" rule — on its own: a name guessed wrong (no Product default, no
153
- // origin/HEAD) must not stop the capture branches from arriving. Its failure changes nothing.
154
- const def = defaultRef(root, git).name;
155
- if (fetched === 'done') spawnSync('git', ['-c', 'http.lowSpeedLimit=1000', '-c', 'http.lowSpeedTime=20', 'fetch', '--quiet', '--no-tags', 'origin', `+refs/heads/${def}:refs/remotes/origin/${def}`], opts);
156
- }
157
- }
160
+ const fetched = noFetch ? 'skipped' : fetchCaptures(root, env);
158
161
  if (fetched === 'local') info('no remote named origin — only your own captures can be read here');
159
162
  if (fetched === 'failed') warn('could not fetch the capture branches — showing what was last fetched, which may be old');
160
163
  const { claims, defaultBranch } = readClaims(root, { env, now, epics: epic ? new Set([epic]) : null });
package/cli/detect.mjs CHANGED
@@ -4,8 +4,8 @@
4
4
  // yad detect [--json] [--dir <folder>]
5
5
  //
6
6
  // Worked out fresh on every run and never saved: nothing reads it yet except a person, and a saved copy
7
- // would be a new file shape that goes stale the moment someone installs a skill. E51 (bind per profile)
8
- // and E85 (the toolbox check) are the readers this is built for.
7
+ // would be a new file shape that goes stale the moment someone installs a skill. `yad skill list` /
8
+ // `yad skill bind` (E51, "is this skill installed here?") and E85 (the toolbox check) read it too.
9
9
  //
10
10
  // TWO PLACES ARE LOOKED IN: the folder the command runs on (what a teammate who clones it also gets), and
11
11
  // the user's home folder (where most people install skills). Every item says which one, and which agents
package/cli/docs.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  // documentation sites generated by the yad-docs / yad-docs-overview skills. The CONTENT generation
3
3
  // (reading epic/architecture/contract/ui/stories → writing src/data/*.ts and theming index.css) is
4
4
  // the AI step inside those skills; this module only does the mechanical parts: npm build (a
5
- // subprocess, like yad-spec shelling npx repomix), the platform Pages wiring (reusing platform.mjs),
5
+ // subprocess, like yad-spec shelling out to Repomix), the platform Pages wiring (reusing platform.mjs),
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
  //
package/cli/doctor.mjs CHANGED
@@ -10,7 +10,7 @@ import { VERSION, BACKUP_SUFFIX, MIRRORED_FILES, mirrorDrift, PROJECT_FILES, MOD
10
10
  import { mergeHookSettings, hookMatcherFires, ideTargetsFor, safeIdeTargetStateFor, hookScriptReady, miswiredGuardCommand, gitHookState, legacyModuleActions, legacyProductActions, renamedNameHits, PRODUCT_CHECK_WORKFLOWS, productProfileEffect, workflowsPassingProduct, productGateBlockers, oldProfileAdvice, gateProfileFix } from './plan.mjs';
11
11
  import { hasSiblingRepo, workspaceFileState, WORKSPACE_FILE } from './find-product.mjs';
12
12
  import { planMigration } from './migrate.mjs';
13
- import { ADVANCE_VALUES, isGateStep, killSwitchOn, loadAutomation, stepDef as catalogueStep, loadLedger, owedSteps, epicIds, epicRel, epicRoot, FOUNDATION_DIR, FOUNDATION_EPIC, DISCOVERY_EPIC, staleFoundationGuards, unwrittenSections, artifactBase, artifactAgrees, epicStories, laneStarted, isValidEpicId, epicLineage, isGenesisType, readFrontmatter, resolveThread, stateInvariants, contractSurfaceHash, acceptedHashes, isStaleHash, workItemType, WORK_ITEM_TYPES, themeOf, themeKey, stepPhase, stepDef, matchLifecycleProfile, lifecycleProfile, LIFECYCLE_PROFILES, SENTINELS, normalizeBindings, optionalStepsFor, isSkippableStep, recordedRouteDisagrees, isPassed, stepStatus, claimsSkipped, STEP_STATES, isStepRecord, RECORDED_STEP_STATES, productDriftPairs } from './epic-state.mjs';
13
+ import { ADVANCE_VALUES, isGateStep, killSwitchOn, loadAutomation, stepDef as catalogueStep, loadLedger, owedSteps, epicIds, epicRel, epicRoot, FOUNDATION_DIR, FOUNDATION_EPIC, DISCOVERY_EPIC, staleFoundationGuards, unwrittenSections, artifactBase, artifactAgrees, epicStories, laneStarted, isValidEpicId, epicLineage, isGenesisType, readFrontmatter, resolveThread, stateInvariants, contractSurfaceHash, acceptedHashes, isStaleHash, workItemType, WORK_ITEM_TYPES, themeOf, themeKey, stepPhase, stepDef, matchLifecycleProfile, lifecycleProfile, LIFECYCLE_PROFILES, SENTINELS, normalizeBindings, stepsReadingBinding, optionalStepsFor, isSkippableStep, recordedRouteDisagrees, isPassed, stepStatus, claimsSkipped, STEP_STATES, isStepRecord, RECORDED_STEP_STATES, productDriftPairs } from './epic-state.mjs';
14
14
  import { loadDebt } from './thread.mjs';
15
15
  import { readShips } from './ledger.mjs';
16
16
  import { gitHead, insideWorkspace } from './setup.mjs';
@@ -25,6 +25,7 @@ import { soloTeamHint, TEAM_CMD } from './people.mjs';
25
25
  import { indexFreshness, INDEX_FILE } from './product-index.mjs';
26
26
  import { productGit, resolveDefaultBranch } from './productcommit.mjs';
27
27
  import { readOwners } from './owners.mjs';
28
+ import { readMembers, judgedMemberFiles } from './members.mjs';
28
29
  import { loadChoices, readToolboxFile } from './toolbox.mjs';
29
30
 
30
31
  // A registered path doctor may run git in (E81): a checkout the judgement accepts — never a refused
@@ -1889,6 +1890,37 @@ export function ownerChecks(checks, root) {
1889
1890
  'such a file is ignored — no owner is shown and the edit-time warning is off. For a step on the chain, `yad assign <epic> <step> --force` replaces it and `yad unassign <epic> <step> --force` removes it; for any other, delete it (`git rm <path>`). `yad owners` lists them all');
1890
1891
  }
1891
1892
 
1893
+ // `members:*` (E131). A member file that cannot be used, or an account or email two files share, pairs
1894
+ // nothing — the person it was meant to join counts twice, and nobody is told anywhere else. And where no
1895
+ // CI runs (a local ledger, no platform), nothing checks a member file: say that it is its owner's statement.
1896
+ export function memberChecks(checks, root) {
1897
+ let got;
1898
+ try { got = readMembers(root); } catch { return; }
1899
+ const some = (list) => `${list.slice(0, 3).join('; ')}${list.length > 3 ? ` (+${list.length - 3} more)` : ''}`;
1900
+ if (got.errors.length) {
1901
+ check(checks, 'members:unreadable', 'project', 'warn', `${got.errors.length} member file(s) pair nothing: ${some(got.errors.map((e) => e.error))}`,
1902
+ 'such a file joins no email to any login, so its person counts twice and is not on the team list. Its owner runs `yad member add` to write it again; otherwise delete it (`yad member remove <login> --reason …`)');
1903
+ }
1904
+ if (got.duplicates.length) {
1905
+ check(checks, 'members:duplicate', 'project', 'warn', `an account or email is in two member files: ${some(got.duplicates)}`,
1906
+ 'one account and one email belong to one member. Until one file is fixed, neither file joins it. On a verified Product the member-check gate refuses the PR that adds the second');
1907
+ }
1908
+ // Which files the gate COUNT leaves out, and why (review 3): no live gate, or a file the gate never judged.
1909
+ if (got.members.length && got.identity.verified && got.identity.platform === 'github') {
1910
+ const judged = judgedMemberFiles(root, got.identity);
1911
+ const left = got.members.filter((m) => !judged.files.has(m.rel)).map((m) => m.rel);
1912
+ if (left.length) {
1913
+ check(checks, 'members:untrusted', 'project', 'warn', `${left.length} member file(s) are not used by the active-people count: ${left.slice(0, 3).join('; ')}${left.length > 3 ? ` (+${left.length - 3} more)` : ''}`,
1914
+ judged.live ? 'no gate as shipped judged them: each was last changed before the member-check gate or its workflow last changed on origin\'s default branch (every `yad update` that changes either resets this), or differs here from origin — its owner runs `yad member add` again, and that change is judged'
1915
+ : 'the member-check gate or its workflow on origin\'s default branch is missing, edited, or from another yadflow version than this one — `yad update` installs the shipped copies, then each member runs `yad member add` again');
1916
+ }
1917
+ }
1918
+ if (got.members.length && !got.identity.verified) {
1919
+ check(checks, 'members:unchecked', 'project', 'ok', `${got.members.length} member file(s), and no CI checks them on this Product`,
1920
+ 'with a local ledger or no platform, no member-check gate runs, so each member file is its owner\'s own statement');
1921
+ }
1922
+ }
1923
+
1892
1924
  // The installed gates whose changed list must name a rename by both paths (E114), and the test for one
1893
1925
  // that does not. Read as bash reads it, one command at a time, so one fixed command never hides its blind
1894
1926
  // twin: a `#` line is prose and is dropped FIRST (a `\` at its end does not go on); then a line ending in
@@ -2099,14 +2131,86 @@ export function skillBindingChecks(checks, root) {
2099
2131
  `${rel} binds ${gates.join(', ')}, which no skill runs — review gates are driven by \`yad gate\``,
2100
2132
  'bind the author step instead (for example `architecture`, not `architecture-review`)');
2101
2133
  }
2134
+ const routeBound = profileBindingChecks(checks, rel, raw.profiles);
2135
+ if (routeBound === null) return;
2102
2136
  // Its OWN id, not `skills` again. A file with one good binding and one broken line fires both, and
2103
2137
  // two checks sharing an id put a green tick under the complaint in prose — and, worse, let a `--json`
2104
2138
  // consumer keying by id overwrite the warning with the tick.
2105
- if (bound.length) {
2139
+ if (bound.length || routeBound) {
2106
2140
  const chained = bound.filter(([, list]) => list.length > 1).length;
2107
2141
  check(checks, 'skills:bound', 'project', 'ok',
2108
- `skills: ${bound.length} step(s) bound${chained ? `, ${chained} to more than one skill` : ''}`);
2142
+ `skills: ${bound.length} step(s) bound${chained ? `, ${chained} to more than one skill` : ''}`
2143
+ + (routeBound ? `; ${routeBound} more for one route only` : ''));
2144
+ }
2145
+ }
2146
+
2147
+ // `profiles.<p>.steps` (E51): the same three "your line did nothing" warnings, each naming its route,
2148
+ // plus the two only a route can have — a route this release does not carry, and a step the route never
2149
+ // walks (`architecture` on `spike`: no epic on it ever reaches that step, which is why `yad skill bind`
2150
+ // refuses the same line). Returns how many lines bind something, or null after a fail.
2151
+ function profileBindingChecks(checks, rel, profiles) {
2152
+ if (profiles === undefined) return 0;
2153
+ if (!isPlainObject(profiles)) {
2154
+ check(checks, 'skills:profiles', 'project', 'fail', `${rel}: \`profiles\` must be a JSON object [YAD-STATE-002]`,
2155
+ 'expected `"profiles": { "<profile>": { "steps": { "<step-id>": "<skill>" } } }`');
2156
+ return null;
2157
+ }
2158
+ const broken = Object.entries(profiles)
2159
+ .filter(([, v]) => !isPlainObject(v) || (v.steps !== undefined && !isPlainObject(v.steps)))
2160
+ .map(([id]) => id);
2161
+ if (broken.length) {
2162
+ check(checks, 'skills:profiles', 'project', 'fail',
2163
+ `${rel}: ${broken.map((id) => `\`profiles.${id}\``).join(', ')} must hold a \`steps\` object [YAD-STATE-002]`,
2164
+ 'expected `"<profile>": { "steps": { "<step-id>": "<skill>" } }`');
2165
+ return null;
2166
+ }
2167
+ const bindings = normalizeBindings({ profiles });
2168
+ const unusable = [];
2169
+ const unknownStep = [];
2170
+ const gates = [];
2171
+ const offRoute = [];
2172
+ const unknownRoute = [];
2173
+ let bound = 0;
2174
+ for (const [id, entry] of Object.entries(profiles)) {
2175
+ const kept = (Object.hasOwn(bindings.profiles, id) ? bindings.profiles[id].steps : null) || {};
2176
+ const route = lifecycleProfile(id);
2177
+ if (!route && Object.keys(kept).length) unknownRoute.push(id);
2178
+ for (const step of Object.keys(entry.steps || {})) {
2179
+ if (!Object.hasOwn(kept, step)) { unusable.push(`${id}: ${step}`); continue; }
2180
+ bound += 1;
2181
+ const def = stepDef(step);
2182
+ if (!def) unknownStep.push(`${id}: ${step}`);
2183
+ else if (!def.skill) gates.push(`${id}: ${step}`);
2184
+ else if (route && (def.phase === 'build' ? route.level !== 'feature'
2185
+ : !stepsReadingBinding(step).some((s) => route.rows.some((r) => r.id === s)))) offRoute.push(`${id}: ${step}`);
2186
+ }
2187
+ }
2188
+ if (unusable.length) {
2189
+ check(checks, 'skills:profile-unusable', 'project', 'warn',
2190
+ `${rel}: ${unusable.length} route binding(s) name no skill and are ignored — ${unusable.join(', ')} [YAD-CFG-006]`,
2191
+ 'each value must be a skill name or a non-empty list of them');
2192
+ }
2193
+ if (unknownRoute.length) {
2194
+ check(checks, 'skills:unknown-profile', 'project', 'warn',
2195
+ `${rel} binds skills for ${unknownRoute.length} route(s) this yadflow does not have: ${unknownRoute.join(', ')}`,
2196
+ `check the spelling against the routes (${LIFECYCLE_PROFILES.map((p) => p.id).join(', ')}), or upgrade yadflow if the route is from a newer release`);
2197
+ }
2198
+ if (unknownStep.length) {
2199
+ check(checks, 'skills:profile-unknown-step', 'project', 'warn',
2200
+ `${rel} binds ${unknownStep.length} route step(s) this yadflow does not run: ${unknownStep.join(', ')}`,
2201
+ 'check the spelling against `yad skill list`, or upgrade yadflow if the step is from a newer release');
2202
+ }
2203
+ if (gates.length) {
2204
+ check(checks, 'skills:profile-review-step', 'project', 'warn',
2205
+ `${rel} binds ${gates.join(', ')}, which no skill runs — review gates are driven by \`yad gate\``,
2206
+ 'bind the author step instead (for example `architecture`, not `architecture-review`)');
2207
+ }
2208
+ if (offRoute.length) {
2209
+ check(checks, 'skills:off-route', 'project', 'warn',
2210
+ `${rel} binds ${offRoute.join(', ')}, a step that route never walks — no epic on it reaches the step`,
2211
+ 'bind it for a route that has the step, or project-wide under `steps`');
2109
2212
  }
2213
+ return bound;
2110
2214
  }
2111
2215
 
2112
2216
  // `.sdlc/toolbox.json`: which toolbox tools this Product uses or skips, and its own tools (E86). Absent is
@@ -2491,6 +2595,7 @@ export function collectDoctor(root, { headCount = null } = {}) {
2491
2595
  skipChecks(checks, root);
2492
2596
  stepStateChecks(checks, root);
2493
2597
  ownerChecks(checks, root);
2598
+ memberChecks(checks, root);
2494
2599
  ownerGuardChecks(checks, root);
2495
2600
  renamedChecks(checks, root);
2496
2601
  productProfileChecks(checks, root);