yadflow 3.18.1 → 4.0.0-next.1

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 (156) hide show
  1. package/CHANGELOG.md +355 -0
  2. package/README.md +79 -26
  3. package/bin/commands.mjs +41 -0
  4. package/bin/yad.mjs +437 -124
  5. package/cli/artifact-status.mjs +34 -15
  6. package/cli/checkpoint.mjs +69 -49
  7. package/cli/codeowners-command.mjs +170 -0
  8. package/cli/codeowners.mjs +397 -0
  9. package/cli/commit.mjs +13 -9
  10. package/cli/companion.mjs +2 -2
  11. package/cli/dial.mjs +183 -0
  12. package/cli/docs.mjs +88 -32
  13. package/cli/doctor.mjs +1472 -97
  14. package/cli/epic-state.mjs +3478 -232
  15. package/cli/epic.mjs +506 -0
  16. package/cli/errors.mjs +4 -1
  17. package/cli/gate.mjs +1002 -209
  18. package/cli/history.mjs +556 -0
  19. package/cli/hook.mjs +266 -55
  20. package/cli/hubcommit.mjs +6 -17
  21. package/cli/index-command.mjs +87 -0
  22. package/cli/ledger.mjs +57 -7
  23. package/cli/lib.mjs +184 -18
  24. package/cli/manifest.mjs +367 -56
  25. package/cli/migrate.mjs +726 -53
  26. package/cli/mode.mjs +170 -0
  27. package/cli/next.mjs +349 -90
  28. package/cli/openpr.mjs +191 -39
  29. package/cli/people.mjs +654 -0
  30. package/cli/plan.mjs +417 -132
  31. package/cli/platform.mjs +110 -129
  32. package/cli/product-index.mjs +287 -0
  33. package/cli/protection.mjs +706 -0
  34. package/cli/reconcile.mjs +38 -12
  35. package/cli/repo-publish.mjs +24 -26
  36. package/cli/repo.mjs +23 -14
  37. package/cli/report.mjs +21 -15
  38. package/cli/review.mjs +24 -27
  39. package/cli/riskmap-command.mjs +289 -0
  40. package/cli/riskmap.mjs +373 -0
  41. package/cli/setup.mjs +139 -287
  42. package/cli/ship.mjs +7 -6
  43. package/cli/skill.mjs +180 -0
  44. package/cli/skip.mjs +211 -30
  45. package/cli/thread.mjs +42 -17
  46. package/cli/tidy.mjs +20 -20
  47. package/cli/update-commit.mjs +22 -22
  48. package/cli/usage.mjs +115 -109
  49. package/package.json +3 -3
  50. package/skills/sdlc/config.yaml +166 -87
  51. package/skills/sdlc/module-help.csv +35 -35
  52. package/skills/yad-analysis/SKILL.md +125 -65
  53. package/skills/yad-architecture/SKILL.md +34 -23
  54. package/skills/yad-architecture/references/contract-format.md +10 -8
  55. package/skills/yad-backfill/SKILL.md +14 -8
  56. package/skills/yad-backfill/references/backfill.md +1 -1
  57. package/skills/yad-change/SKILL.md +127 -52
  58. package/skills/yad-change/references/triage.md +42 -28
  59. package/skills/yad-checks/SKILL.md +89 -45
  60. package/skills/yad-checks/references/check-gates.md +315 -92
  61. package/skills/yad-checks/templates/checks/build-test-lint.sh +25 -7
  62. package/skills/yad-checks/templates/checks/commit-message.sh +17 -3
  63. package/skills/yad-checks/templates/checks/contract-check.sh +58 -2
  64. package/skills/yad-checks/templates/checks/epic-open.sh +3 -3
  65. package/skills/yad-checks/templates/checks/install-deps.sh +46 -0
  66. package/skills/yad-checks/templates/checks/ledger-guard.sh +94 -18
  67. package/skills/yad-checks/templates/checks/lineage-check.sh +23 -9
  68. package/skills/yad-checks/templates/checks/package-manager.sh +140 -0
  69. package/skills/yad-checks/templates/checks/reconcile-debt-check.sh +4 -4
  70. package/skills/yad-checks/templates/checks/risk-map-check.sh +438 -0
  71. package/skills/yad-checks/templates/checks/verified-commits.sh +20 -46
  72. package/skills/yad-checks/templates/github/yad-checks.yml +37 -5
  73. package/skills/yad-checks/templates/github/yad-hub-checks.yml +5 -5
  74. package/skills/yad-checks/templates/github/yad-update-guard.yml +3 -4
  75. package/skills/yad-checks/templates/github/yad-verified-commits.yml +4 -4
  76. package/skills/yad-checks/templates/gitlab/.gitlab-ci.yml +7 -1
  77. package/skills/yad-checks/templates/gitlab/yad-checks.gitlab-ci.yml +22 -4
  78. package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +5 -5
  79. package/skills/yad-checks/templates/gitlab/yad-verified-commits.gitlab-ci.yml +4 -4
  80. package/skills/yad-checks/templates/hooks/ledger-guard-cursor.sh +91 -0
  81. package/skills/yad-checks/templates/hooks/ledger-guard.sh +38 -7
  82. package/skills/yad-commit/SKILL.md +6 -6
  83. package/skills/yad-connect-design/SKILL.md +6 -6
  84. package/skills/yad-connect-design/references/design-context.md +1 -1
  85. package/skills/yad-connect-design/references/design-registry.md +2 -2
  86. package/skills/yad-connect-docs/SKILL.md +12 -12
  87. package/skills/yad-connect-docs/references/docs-registry.md +1 -1
  88. package/skills/yad-connect-learning/SKILL.md +5 -5
  89. package/skills/yad-connect-learning/references/learning-registry.md +2 -2
  90. package/skills/yad-connect-repos/SKILL.md +92 -54
  91. package/skills/yad-connect-repos/references/code-context.md +6 -6
  92. package/skills/yad-connect-repos/references/hub-config.md +68 -58
  93. package/skills/yad-connect-repos/references/repos-registry.md +10 -9
  94. package/skills/yad-connect-repos/references/risk-map.md +81 -0
  95. package/skills/yad-connect-testing/SKILL.md +6 -6
  96. package/skills/yad-connect-testing/references/testing-context.md +3 -4
  97. package/skills/yad-connect-testing/references/testing-registry.md +2 -2
  98. package/skills/yad-defects/SKILL.md +8 -8
  99. package/skills/yad-discovery/SKILL.md +130 -94
  100. package/skills/yad-discovery/references/discovery-schema.md +23 -7
  101. package/skills/yad-discovery/references/foundation-schema.md +374 -0
  102. package/skills/yad-docs/SKILL.md +16 -11
  103. package/skills/yad-docs/references/data-mapping.md +9 -7
  104. package/skills/yad-docs/templates/app/package-lock.json +3 -3
  105. package/skills/yad-docs-overview/SKILL.md +32 -17
  106. package/skills/yad-docs-overview/references/pipeline-model.md +47 -28
  107. package/skills/yad-docs-sync/SKILL.md +10 -5
  108. package/skills/yad-docs-sync/references/staleness.md +8 -7
  109. package/skills/yad-engineer-review/SKILL.md +88 -24
  110. package/skills/yad-engineer-review/references/ship-and-record.md +25 -16
  111. package/skills/yad-epic/SKILL.md +178 -100
  112. package/skills/yad-epic/references/state-schema.md +626 -117
  113. package/skills/yad-hub-bridge/SKILL.md +66 -48
  114. package/skills/yad-hub-bridge/references/bridge.md +110 -83
  115. package/skills/yad-hub-bridge/references/login-roster.md +163 -70
  116. package/skills/yad-hub-bridge/templates/checks/hub-route.sh +22 -19
  117. package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +34 -14
  118. package/skills/yad-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +2 -2
  119. package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +22 -12
  120. package/skills/yad-implement/SKILL.md +29 -15
  121. package/skills/yad-implement/references/implement-conventions.md +2 -2
  122. package/skills/yad-learn/SKILL.md +9 -9
  123. package/skills/yad-learn/references/learning-state.md +2 -2
  124. package/skills/yad-open-pr/SKILL.md +64 -29
  125. package/skills/yad-pair-review/SKILL.md +18 -16
  126. package/skills/yad-pair-review/references/session-state.md +4 -4
  127. package/skills/yad-pr-template/SKILL.md +48 -27
  128. package/skills/yad-pr-template/references/risk-routing.md +97 -24
  129. package/skills/yad-pr-template/templates/checks/pr-template.sh +37 -15
  130. package/skills/yad-pr-template/templates/checks/pr-title.sh +27 -13
  131. package/skills/yad-pr-template/templates/checks/risk-route.sh +107 -14
  132. package/skills/yad-pr-template/templates/github/pull_request_template.md +7 -5
  133. package/skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md +7 -5
  134. package/skills/yad-pr-template/templates/hub/github/pull_request_template.md +15 -14
  135. package/skills/yad-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +15 -13
  136. package/skills/yad-reconcile/SKILL.md +3 -3
  137. package/skills/yad-report/SKILL.md +5 -5
  138. package/skills/yad-review-companion/SKILL.md +12 -9
  139. package/skills/yad-review-gate/SKILL.md +198 -79
  140. package/skills/yad-review-gate/references/gating.md +230 -54
  141. package/skills/yad-run/SKILL.md +86 -56
  142. package/skills/yad-run/references/run-loop.md +67 -45
  143. package/skills/yad-ship/SKILL.md +18 -14
  144. package/skills/yad-spec/SKILL.md +31 -17
  145. package/skills/yad-spec/references/spec-handoff.md +17 -5
  146. package/skills/yad-status/SKILL.md +114 -56
  147. package/skills/yad-stories/SKILL.md +42 -27
  148. package/skills/yad-stories/references/story-schema.md +10 -9
  149. package/skills/yad-stub/SKILL.md +59 -48
  150. package/skills/yad-sync-repos/SKILL.md +3 -3
  151. package/skills/yad-test-cases/SKILL.md +37 -30
  152. package/skills/yad-test-cases/references/test-cases-schema.md +8 -5
  153. package/skills/yad-timeline/SKILL.md +8 -7
  154. package/skills/yad-ui/SKILL.md +46 -25
  155. package/cli/roster.mjs +0 -164
  156. package/skills/sdlc/install.sh +0 -68
@@ -5,7 +5,7 @@ description: 'Brings every connected code repo up to date in one shot: switches
5
5
 
6
6
  # SDLC — Sync Connected Repos (one command, every repo on its default branch)
7
7
 
8
- **Goal:** Before front/build work starts, get every connected code repo onto its default branch at the
8
+ **Goal:** Before Shape/Build work starts, get every connected code repo onto its default branch at the
9
9
  latest commit, so nobody implements on a stale or wrong branch. This is the deterministic counterpart to
10
10
  the `sync-before-implementation` discipline: one command instead of N manual `git checkout && git pull`.
11
11
 
@@ -15,7 +15,7 @@ tree (branch + fast-forward). It writes **nothing** back to the registry.
15
15
 
16
16
  ## Conventions
17
17
 
18
- - `{project-root}` resolves from the project working directory (the **product hub**).
18
+ - `{project-root}` resolves from the project working directory (the **Product**).
19
19
  - Registry: `{project-root}/.sdlc/repos.json` (project-wide; the same file `yad-connect-repos` writes).
20
20
  Each repo entry supplies `path` (local path, relative to `{project-root}` or absolute) and
21
21
  `default_branch` (the branch to land on).
@@ -64,7 +64,7 @@ Per repo: `switched to <branch>, pulled (ff)` / `already current` / `SKIPPED (..
64
64
  so any repo whose `HEAD` now differs from its registry `syncedHead` has a **stale code-context pack** —
65
65
  the command ends by pointing the human at `yad repo refresh` to repack (that is a separate human
66
66
  decision; this skill never repacks or writes the registry). After the repack + AI code-map
67
- regeneration, `yad repo refresh --push` publishes the refreshed code-maps + registry to the hub's
67
+ regeneration, `yad repo refresh --push` publishes the refreshed code-maps + registry to the Product's
68
68
  default branch as a `chore(hub): sync code-context … [skip ci]` audit commit.
69
69
 
70
70
  ## Hard rules
@@ -1,24 +1,23 @@
1
1
  ---
2
2
  name: yad-test-cases
3
- description: 'Front state 9 of the gated SDLC — a PARALLEL, non-blocking track. Opens when the stories gate passes (the epic is already ready-for-build, so the build half can start at the same time) and runs alongside implementation. With the test architect (Murat), author test-cases.md for the approved stories, and — when a testing tool is connected — generate/link the actual automation tests in it; otherwise produce the test-case artifact only. Reads epic + architecture + contract + UI + stories as input. Never auto-advances — hands off to the team review gate. Use when the user says "author the test cases" or after the stories gate passes.'
3
+ description: 'Shape step 9 of the gated SDLC — a PARALLEL, non-blocking track. Opens when the stories gate passes (the epic is already ready-for-build, so Build can start at the same time) and runs alongside implementation. With the test architect, author test-cases.md for the approved stories, and — when a testing tool is connected — generate/link the actual automation tests in it; otherwise produce the test-case artifact only. Reads epic + architecture + contract + UI + stories as input. Never auto-advances — hands off to the team review gate. Use when the user says "author the test cases" or after the stories gate passes.'
4
4
  ---
5
5
 
6
- # SDLC — Author Test Cases (front state 9 — parallel, non-blocking)
6
+ # SDLC — Author Test Cases (Shape step 9 — parallel, non-blocking)
7
7
 
8
8
  **Goal:** Produce a human-authored, AI-assisted `test-cases.md` for an approved epic — the risk-based
9
9
  test cases that cover the stories' acceptance criteria — **and**, when a testing tool is connected, the
10
10
  **actual automation tests** inside the connected code repo(s), linked back from the artifact. This is a
11
- **front state**: human-authored with AI assist, **never auto-advances**. When the test cases are
12
- drafted, control passes to `yad-review-gate` (base rule: owner + 1 reviewer).
11
+ **Shape step**: human-authored with AI assist, **never auto-advances**. When the test cases are
12
+ drafted, control passes to `yad-review-gate` (base rule: 1 distinct approver, who should not be the author).
13
13
 
14
- **This step does NOT block the build half.** It opens when the **stories** gate passes — at which point
14
+ **This step does NOT block Build.** It opens when the **stories** gate passes — at which point
15
15
  the epic is already `ready-for-build`, so implementation (`yad-spec` → `yad-implement` → …) can start
16
16
  **at the same time** the tester works here. The test-cases track is driven by its own step `status`
17
17
  (it opens to `in_progress` when `stories-review` passes) and its review **never moves `currentStep`
18
18
  away from `ready-for-build`**, so the two run in parallel.
19
19
 
20
- Test work is shaped by the **test architect** lens — **Murat** (`bmad-tea`), driving
21
- `bmad-testarch-test-design` for the cases and `bmad-testarch-automate` for the automation. The
20
+ Test work is shaped by the **test architect** lens, which designs the cases and writes the automation. The
22
21
  automation is materialized in the **testing tool connected via `yad-connect-testing`**
23
22
  (`.sdlc/testing.json`), reached through its MCP. When a tool is connected the lens **generates** tests
24
23
  into it (or **links** an existing suite and reads it back); when none is connected, the step degrades to
@@ -31,7 +30,7 @@ the Markdown artifact only — the testing tool is additive, exactly like the de
31
30
  - The connected testing tool is recorded in `{project-root}/.sdlc/testing.json` (`config.yaml`
32
31
  `testing`), written by `yad-connect-testing`. The per-epic case→test map is `test-links.json`
33
32
  (Step 4b).
34
- - Speak in the configured `communication_language`; write documents in `document_output_language`.
33
+ - Speak in the `communication_language` set in `{project-root}/.sdlc/config.yaml`; write documents in `document_output_language`.
35
34
 
36
35
  ## On Activation
37
36
 
@@ -44,14 +43,14 @@ it passes. The check is track-aware: it keys off the `test-cases` step's predece
44
43
  `currentStep` (this is a parallel track — `currentStep` stays at `ready-for-build`).
45
44
 
46
45
  This passes once the `test-cases` step is runnable — its predecessor `stories-review` is `done` (so the
47
- step has opened to `in_progress`) and `test-cases` is not already `done`. While it is still `blocked`,
48
- the stories review has not passed.
46
+ step has opened to `in_progress`) and `test-cases` is not already `done`. While it is still `todo` (or
47
+ `blocked`, in a file written before shape 7), the stories review has not passed.
49
48
 
50
49
  ### Step 1b — Open the authoring branch
51
50
  Open the test-cases authoring branch `test-cases/EP-<slug>` per the shared procedure
52
51
  (`../yad-epic/references/state-schema.md` → "Authoring branches"): git-safe (skip with a note if
53
52
  `{project-root}` is not a git work tree), check out the branch if it exists, else create it from the
54
- hub's default branch. Author and commit `test-cases.md` on it. This is **distinct** from the bridge's
53
+ Product's default branch. Author and commit `test-cases.md` on it. This is **distinct** from the verified ledger's
55
54
  `review/…` branch.
56
55
 
57
56
  ### Step 2 — Read inputs
@@ -62,7 +61,9 @@ for the cases — the test cases must collectively verify every story's criteria
62
61
 
63
62
  - **UI-optional-safe:** `ui-design.md` may be absent — the `ui-design` step is optional and can be
64
63
  marked N/A for a UI-less epic (state.json shows it `skipped`). When there is no UI design, there are
65
- simply no screens to exercise; the stories' acceptance criteria remain the source of truth.
64
+ simply no screens to exercise; the stories' acceptance criteria remain the source of truth. If it
65
+ shows `deferred`, the screens are still to be designed: cover the stories' criteria now, and leave the
66
+ screen-level cases for when the UI design lands.
66
67
 
67
68
  ### Step 2b — Load existing-code context (make the brain code-aware)
68
69
  Read the registry `{project-root}/.sdlc/repos.json` (`config.yaml` `code_context`). For **each repo in
@@ -77,11 +78,11 @@ inventing parallel ones.
77
78
  - **Traceability:** record the loaded maps in the `test-cases.md` `code-context:` frontmatter field.
78
79
 
79
80
  ### Step 3 — Author the test cases (assist: test architect)
80
- Adopt the **test architect** lens (`bmad-tea`, Murat), driving `bmad-testarch-test-design`. For each
81
+ Adopt the **test architect** lens. For each
81
82
  story / user flow shape risk-based test cases:
82
83
 
83
84
  - **Risk assessment** — categorize and score what can fail (probability × impact); depth scales with
84
- impact (Murat's "risk-based testing" principle).
85
+ impact (risk-based testing).
85
86
  - **Coverage plan** — assign each case a priority **P0 (critical) / P1 (high) / P2 (medium) / P3 (low)**
86
87
  and a level, preferring the lowest useful level (unit > integration > E2E).
87
88
  - **Entry/exit criteria** and any **NFR** thresholds (security, performance, reliability) in scope.
@@ -95,8 +96,8 @@ Read `{project-root}/.sdlc/testing.json` (`config.yaml` `testing.registry`). Dec
95
96
  (mirrors the `design: none` degrade). Skip to Step 4.
96
97
  - **A tool is connected and its MCP is available:** adopt the `test architect` lens and, using the
97
98
  provider recorded in `testing.json` (Playwright via a Playwright MCP, Cypress/pytest/Maestro via
98
- theirs — Maestro authors mobile flows where Playwright has no reach) drive
99
- `bmad-testarch-automate`:
99
+ theirs — Maestro authors mobile flows where Playwright has no reach) write the
100
+ automation:
100
101
  - **Generate** — when the provider is write-capable, author one automation test per high-priority
101
102
  (P0/P1) case into the connected code repo(s) for the repos in `epic.repos`, reusing the code-maps
102
103
  (Step 2b) and existing fixtures so tests target built endpoints/components, then run them via the MCP
@@ -163,10 +164,10 @@ degraded (`testing: none`), do **not** write `test-links.json`.
163
164
 
164
165
  ### Step 5 — Advance the authoring step (NOT the gate)
165
166
  **Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/hub.json`:
166
- **bridge mode** is `platform` set AND `bridge_enabled` (or legacy `bridge`) `true`.
167
+ **verified mode** is `platform` set AND `ledger: "verified"` — or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or legacy `bridge`) `true`. `ledger` wins whenever it is present.
167
168
 
168
- **Bridge mode — do NOT write `state.json`.** The ledger is CI-owned: the `ledger-guard` check rejects
169
- any non-bot commit touching `epics/*/.sdlc/{state,approvals,comments,hub-prs}.json` or
169
+ **verified mode — do NOT write `state.json`.** The ledger is CI-owned: the `ledger-guard` check rejects
170
+ any non-bot commit touching `epics/*/.sdlc/{state,approvals,comments,product-prs,hub-prs}.json` or
170
171
  `epics/*/reviews/*.md`, `yad gate open` deliberately skips this write for the same reason, and
171
172
  `yad gate ci --merged` performs the whole transition when the review PR merges. Making the edit here
172
173
  fails the gate if it rides the review PR, and desynchronises the ledger CI is about to rewrite if it
@@ -174,21 +175,27 @@ is pushed around the gate. Commit the artifact set — **`test-cases.md` and, wh
174
175
  tool was used, `.sdlc/test-links.json`** (artifact-side, not ledger; generated tests live in their
175
176
  own code repo, not here) — then hand off to `yad-review-gate`.
176
177
 
177
- **Otherwise — file-only, or a platform with no gate-sync CI — write it.** In `state.json`: set
178
- `test-cases.status: "done"` and set `test-cases-review.status: "in_review"`. **Leave `currentStep` at
179
- `ready-for-build`** — this is the parallel track; moving `currentStep` would pull it back from the
180
- build half. Write `state.json`. Do **not** touch `approvals.json`. On this branch `yad gate open`
181
- makes the same edit — `markInReview` leaves `currentStep` alone once it is `ready-for-build`
182
- (`cli/epic-state.mjs`) — so it is a no-op once the gate has run.
178
+ **Otherwise — local, or a platform with no gate-sync CI — the engine makes this edit, not you.**
179
+ `yad gate open <epic> test-cases.md` marks `test-cases-review` `in_review`, closes `test-cases` as `done`, and **leaves `currentStep` at `ready-for-build`** — this is the parallel track, and moving it would pull the epic back from Build
180
+ — the same transition, from the one function that owns it (`markInReview`, `cli/epic-state.mjs`).
181
+ `yad-review-gate action: open` runs that command; hand off to it rather than editing the ledger here.
182
+
183
+ **With no platform configured** it writes the ledger and simply opens no PR, so this works offline.
184
+ **With a platform** the `review/EP-<slug>/test-cases` branch must already be **on origin** — the command
185
+ refuses and writes nothing otherwise. Cut it from the authoring branch and push it before handing off
186
+ (or run `yad open-pr` from that branch: it pushes the branch, then delegates).
187
+
188
+ Do **not** hand-edit `state.json`, and do **not** touch `approvals.json` — only real reviewers approve,
189
+ through the gate.
183
190
 
184
191
  ### Step 6 — Stop at the gate (do NOT advance)
185
192
  Report: the path to `test-cases.md`, the connected testing tool and what it produced (e.g. "Playwright —
186
193
  6 tests generated", the suite path + `test-links.json` path, or "no testing tool — artifacts-only"), that
187
- the build half may already be underway in parallel, and that the next action is **review** via
188
- `yad-review-gate` (base rule: owner + 1 reviewer). **Never record approval here.** Front states do not
189
- auto-advance. When the hub has a platform, the gate opens a review
190
- PR on the hub (via `yad-hub-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments
191
- into the ledger; otherwise the review is recorded file-only.
194
+ Build may already be underway in parallel, and that the next action is **review** via
195
+ `yad-review-gate` (base rule: 1 distinct approver, who should not be the author). **Never record approval here.** Shape steps do not
196
+ auto-advance. When the Product has a platform, the gate opens a review
197
+ PR on the Product (via `yad-hub-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments
198
+ into the ledger; otherwise the review is recorded local.
192
199
 
193
200
  ## Reference
194
201
  - Test-cases frontmatter, body template, and the `test-links.json` schema: `references/test-cases-schema.md`.
@@ -1,6 +1,6 @@
1
1
  # Test-cases schema
2
2
 
3
- The test cases authored at front state 9 are one Markdown file under `epics/EP-<slug>/`, named
3
+ The test cases authored at Shape step 9 are one Markdown file under `epics/EP-<slug>/`, named
4
4
  `test-cases.md` (one per epic — mirrors `ui-design.md`, not a per-story folder). The optional automation
5
5
  linkage is a sibling `test-links.json`.
6
6
 
@@ -56,15 +56,18 @@ Written by `yad-test-cases` Step 4b. Absent when the step ran artifacts-only (`t
56
56
  - **Cover every story.** The "Coverage & traceability" section must map each story's acceptance criteria
57
57
  to at least one case — an unmapped criterion is a gap the reviewer should catch.
58
58
  - **Priorities are risk-based.** P0 = critical (run on every commit) … P3 = low (on-demand); depth
59
- scales with impact (Murat's principle).
59
+ scales with impact.
60
60
  - **Prefer the lowest useful level.** unit > integration > E2E when a case can be verified lower.
61
61
  - **`repos` is a subset of `epic.repos`.** Automation cannot target a repo the epic does not declare.
62
62
  - **Single-file, not a folder.** `test-cases.md` resolves through the gate's default content-hash path
63
- (the file's bytes), so the review gate, revoke-on-change, and review-PR machinery work unchanged.
63
+ (the file without its frontmatter `status:` line), so the review gate, revoke-on-change, and review-PR machinery work unchanged.
64
64
  - **Automation is additive.** When no testing tool is connected, the step still produces a complete
65
65
  `test-cases.md`; `testing: none` and no `test-links.json`.
66
66
 
67
67
  ## Review routing (the test-cases gate)
68
68
 
69
- The test-cases gate uses the **base rule** (owner + 1 reviewer) — it is not escalated. The natural
70
- reviewer is the test architect / QA owner, but the gate predicate requires only owner + 1.
69
+ The test-cases gate uses the **base rule**: at least 1 distinct approver, who should not be the author.
70
+ The step carries no risk tags, so its full count is also 1. The natural reviewer is the test architect /
71
+ QA owner, but the gate names no role — any approver counts. yadflow does not itself check that the
72
+ approver is not the author (see `yad-review-gate`). The review PR requests no reviewers; the team asks
73
+ them on the PR itself.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yad-timeline
3
- description: 'Phase 6 output enrichment (never a gate) — render a feature THREAD as an evolution view AND resolve its current truth. Walks the thread (genesis -> changes -> defects, linked by parent: frontmatter), renders the evolution as the vendored React/Vite/Tailwind shell HTML + a TIMELINE.md summary (each node: kind, what it re-authored vs inherited, ship events, contract re-lock history, open debt), and emits thread-resolved.md — the composed CURRENT-TRUTH map (the latest epic that owns each artifact + the resolved contract-lock hash). That resolved map is the source-of-truth AI/humans read to plan the next change, instead of a stale genesis epic. Degrades to markdown-only when no docs target is connected. Use when the user says "show the feature timeline", "how did this feature evolve", "resolve the current truth", or "render the thread".'
3
+ description: 'Phase 6 output enrichment (never a gate) — render a feature THREAD as an evolution view AND resolve its current truth. Walks the thread (genesis -> changes -> defects, linked by parent: frontmatter), renders the evolution as the vendored React/Vite/Tailwind shell HTML + a TIMELINE.md summary (each node: type, what it re-authored vs inherited, ship events, contract re-lock history, open debt), and emits thread-resolved.md — the composed CURRENT-TRUTH map (the latest epic that owns each artifact + the resolved contract-lock hash). That resolved map is the source-of-truth AI/humans read to plan the next change, instead of a stale genesis epic. Degrades to markdown-only when no docs target is connected. Use when the user says "show the feature timeline", "how did this feature evolve", "resolve the current truth", or "render the thread".'
4
4
  ---
5
5
 
6
6
  # SDLC — Feature Timeline + Current-Truth Resolver (Phase 6, output enrichment)
@@ -14,9 +14,9 @@ It is an **output enrichment**, exactly like `yad-docs` — **never a gate**: it
14
14
 
15
15
  ## Conventions
16
16
 
17
- - `{project-root}` resolves from the product hub.
17
+ - `{project-root}` resolves from the Product.
18
18
  - Reuses the **`yad-docs` shell** verbatim (`../yad-docs/templates/app/`) — generated `src/data/*.ts`,
19
- themed, deployed via `yad docs deploy`; build-only / markdown-only when no docs target
19
+ themed; build-only / markdown-only (`yad docs deploy` does not build `timeline-site/` yet)
20
20
  (`.sdlc/docs.json`). The resolver part drives the **`yad thread` CLI** (`cli/thread.mjs`).
21
21
  - The thread is **derived** from `parent:` frontmatter (no registry). The thread report lives under the
22
22
  **genesis** epic (`thread == genesis id`).
@@ -32,7 +32,8 @@ It is an **output enrichment**, exactly like `yad-docs` — **never a gate**: it
32
32
 
33
33
  ### Step 1 — Resolve the thread
34
34
  Run `yad thread <thread> --json` (`resolveThread` + `threadEpics` + `resolveCurrentArtifacts`). This
35
- gives the genesis-first chain, each epic's lineage (`kind`, `parent`, `inherits`), the resolved
35
+ gives the genesis-first chain, each epic's lineage (`type`, and `kind` holding the same value under
36
+ the older name, plus `parent` and `inherits`), the resolved
36
37
  current-truth map, and any open reconcile debt. **STOP** and report if the lineage is broken (point at
37
38
  `yad doctor` / `yad-reconcile`).
38
39
 
@@ -54,9 +55,9 @@ deterministically; theme from the design system). The thread maps onto the shell
54
55
  = what it re-authored, its side-effects = the ships it produced + any contract re-lock.
55
56
  - **System components** = the artifacts (epic/architecture/contract/ui/stories/test-cases), each labelled
56
57
  with the epic that currently **owns** it (from the resolved map).
57
- - Label and colour nodes by `kind` — render each node's kind noun (**Change request** / **Defect** /
58
+ - Label and colour nodes by `type` — render each node's type noun (**Change request** / **Defect** /
58
59
  **Hotfix** / **Epic** for feature) alongside its id, not the generic word "epic"; mark sealed epics and
59
- open debt. (A bug is a defect — `kind: defect`. Presentation only; every node is still an epic.)
60
+ open debt. (A bug is a defect — type `defect`. Presentation only; every node is still an epic.)
60
61
 
61
62
  ### Step 4 — Emit `thread-resolved.md` (the current-truth map — derived, non-authoritative)
62
63
  Write `epics/<thread>/thread-resolved.md`: for each artifact base, the **owning epic** (the latest in the
@@ -66,7 +67,7 @@ is the file the next `yad-change` / `yad-epic` reads as "the feature's current t
66
67
 
67
68
  ### Step 5 — Emit `TIMELINE.md` + (optional) deploy
68
69
  Write a short `epics/<thread>/TIMELINE.md` (the chain, what each node changed, ships, open debt) for a
69
- plain-text read — head each node with its kind noun (Change request / Defect / Hotfix / Epic) + id. On `action: deploy`, `yad docs deploy` the site (build-only when no target).
70
+ plain-text read — head each node with its type noun (Change request / Defect / Hotfix / Chore / Epic) + id. On `action: deploy`, note that `yad docs deploy` does not build this folder yet (it builds only the epic `docs-site/` and the overview) — build it with `npm ci` (or `npm install` when it has no lockfile) and `npm run build` inside the folder, and report it as build-only; a failed npm build is reported as a failure, never as a deploy.
70
71
 
71
72
  ## Hard rules
72
73
 
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: yad-ui
3
- description: 'Front state 5 of the gated SDLC. With the ux-designer, author ui-design.md and DESIGN.md for an approved architecture, driving Impeccable as harness slash-commands (document/extract/craft) when installed, or authoring directly when not. Reads epic + architecture as input. Never auto-advances — hands off to the team review gate. Use when the user says "author the UI design" or after the architecture gate passes.'
3
+ description: 'Shape step 5 of the gated SDLC. With the ux-designer, author ui-design.md and DESIGN.md for an approved architecture, driving Impeccable as harness slash-commands (document/extract/craft) when installed, or authoring directly when not. Reads epic + architecture as input. Never auto-advances — hands off to the team review gate. Use when the user says "author the UI design" or after the architecture gate passes.'
4
4
  ---
5
5
 
6
- # SDLC — Author UI Design (front state 5)
6
+ # SDLC — Author UI Design (Shape step 5)
7
7
 
8
8
  **Goal:** Produce a human-authored, AI-assisted `ui-design.md` and `DESIGN.md` for an approved
9
9
  architecture **and**, when a design tool is connected, the **actual feature design** — the mobile
10
10
  screens and/or web pages — inside that tool (e.g. Figma), linked back from the artifacts. This is a
11
- **front state**: human-authored with AI assist, **never auto-advances**. When the UI is drafted, control
12
- passes to `yad-review-gate` (base rule: owner + 1 reviewer).
11
+ **Shape step**: human-authored with AI assist, **never auto-advances**. When the UI is drafted, control
12
+ passes to `yad-review-gate` (base rule: 1 distinct approver, who should not be the author).
13
13
 
14
14
  UI work is shaped by **Impeccable**, invoked as **harness slash-commands** (not a subprocess CLI) per
15
15
  the Phase 0 deviation. If Impeccable is not installed, the `ux-designer` lens authors the same outputs
@@ -28,7 +28,7 @@ like Impeccable.
28
28
  - `DESIGN.md` is Impeccable's conventional root design-system file (RESEARCH-NOTES §4).
29
29
  - The connected design tool is recorded in `{project-root}/.sdlc/design.json` (`config.yaml` `design`),
30
30
  written by `yad-connect-design`. The per-epic screen→frame map is `design-links.json` (Step 4b).
31
- - Speak in the configured `communication_language`; write documents in `document_output_language`.
31
+ - Speak in the `communication_language` set in `{project-root}/.sdlc/config.yaml`; write documents in `document_output_language`.
32
32
 
33
33
  ## On Activation
34
34
 
@@ -45,22 +45,34 @@ This passes when `ui-design` is the next runnable step per the state sequence
45
45
  **This step is OPTIONAL.** If the epic has no user-facing surface (a backend/API service, a data
46
46
  pipeline, infra work), it does not need a UI design. Two signals to watch for:
47
47
 
48
- - **Already skipped:** if `ui-design.status` is `done` with `skipped: true` in `state.json` (the
49
- `--check` above will report `ui-design is already done`), the step was marked N/A — **STOP**, there
50
- is nothing to author; point the user at `yad next EP-<slug>` (the next step is `stories`).
48
+ - **Already skipped or deferred:** if `ui-design.status` is `skipped` in `state.json` — or, in a file
49
+ from before shape 7, `done` with `skipped: true` beside it — the step was marked N/A. If it is
50
+ `deferred`, the team set it aside to come back to later. Either way the `--check` above refuses it
51
+ (`ui-design is already skipped`, or `already deferred`). **STOP**, there is nothing to author now;
52
+ point the user at `yad next EP-<slug>` (the next step is `stories`). To pick a deferred UI back up,
53
+ run `yad undefer EP-<slug> ui-design` first.
51
54
  - **Should be skipped:** if you reach this step and the epic clearly produces no screens, do **not**
52
55
  invent a hollow UI artifact. Offer to mark it N/A instead:
53
- `yad skip EP-<slug> ui-design --reason "<why, e.g. backend-only service>"`. That pre-marks both the
54
- `ui-design` and `ui-design-review` steps `done` (recorded reason + actor), short-circuits the review
55
- gate, and advances to `stories`. It is reversible with `yad skip EP-<slug> ui-design --undo` until the
56
+ `yad skip EP-<slug> ui-design --reason "<why, e.g. backend-only service>"`. That marks both the
57
+ `ui-design` and `ui-design-review` steps `skipped` (recorded reason + actor), short-circuits the review
58
+ gate, and advances to `stories`. It is reversible with `yad unskip EP-<slug> ui-design` until the
56
59
  stories review opens. See `../yad-epic/references/state-schema.md` → "ui-design is optional".
60
+ - **Will have screens, just not yet:** if the epic does need a UI but the team wants to design it later,
61
+ offer `yad defer EP-<slug> ui-design --reason "<why, and who is waiting for it>"` instead of a skip.
62
+ It passes the step for now and keeps it visibly owed. If the team is setting it aside under pressure
63
+ and owes it back, add `--debt`: `yad next` and `yad doctor` then remind them until its review passes.
64
+ It can be picked back up with `yad undefer EP-<slug> ui-design` at any time — after the stories are
65
+ finished, the UI re-opens beside them and the stories stay done.
66
+ - **On a verified Product (the mode check below), none of these verbs works here:** by this step the
67
+ epic's ledger is on the default branch, so `yad skip`, `yad defer` and `yad undefer` refuse (CI owns
68
+ `state.json`, and has no step for them yet). Tell the user that, and do not edit the ledger by hand.
57
69
 
58
70
  ### Step 1b — Open the authoring branch
59
71
  Open the UI authoring branch `ui-design/EP-<slug>` per the shared procedure
60
72
  (`../yad-epic/references/state-schema.md` → "Authoring branches"): git-safe (skip with a note
61
73
  if `{project-root}` is not a git work tree), check out the branch if it exists, else create it from the
62
- hub's default branch. Author and commit `ui-design.md` / `DESIGN.md` on it. This is **distinct** from
63
- the bridge's `review/…` branch.
74
+ Product's default branch. Author and commit `ui-design.md` / `DESIGN.md` on it. This is **distinct** from
75
+ the verified ledger's `review/…` branch.
64
76
 
65
77
  ### Step 2 — Read inputs
66
78
  Read `epic.md` (user-level acceptance signals, scope) and `architecture.md` (flows, components by
@@ -80,7 +92,7 @@ Impeccable is absent, the code-map is the brain's view of what UI/components alr
80
92
  - **Traceability:** record the loaded maps in the `ui-design.md` `code-context:` frontmatter field.
81
93
 
82
94
  ### Step 3 — Shape the UI (assist: ux-designer + Impeccable slash-commands)
83
- Adopt the **ux-designer** lens (`bmad-agent-ux-designer`, Sally). Drive Impeccable as slash-commands:
95
+ Adopt the **ux-designer** lens. Drive Impeccable as slash-commands:
84
96
 
85
97
  - **Existing project** (a codebase/design system already exists): `/impeccable document` → then
86
98
  `/impeccable extract` → then `/impeccable craft`.
@@ -92,7 +104,7 @@ components/tokens into the design system; `/impeccable craft` is shape-then-buil
92
104
  **Graceful degradation:** if Impeccable is not installed (no `/impeccable …` commands available), the
93
105
  `ux-designer` lens authors `ui-design.md` and `DESIGN.md` directly, and you **note in `ui-design.md`
94
106
  that Impeccable was not used**. Do not run `npx impeccable skills install` as part of this step — tool
95
- installation is out of scope for the front half.
107
+ installation is out of scope for Shape.
96
108
 
97
109
  ### Step 3b — Materialize the design in the connected tool (generate or link)
98
110
  Read `{project-root}/.sdlc/design.json` (`config.yaml` `design.registry`). Decide the path:
@@ -172,10 +184,10 @@ Keep the `## Design (<tool>)` section of `ui-design.md` in step with this file.
172
184
 
173
185
  ### Step 5 — Advance the authoring step (NOT the gate)
174
186
  **Check the mode first — the two modes have opposite instructions here.** Read `.sdlc/hub.json`:
175
- **bridge mode** is `platform` set AND `bridge_enabled` (or legacy `bridge`) `true`.
187
+ **verified mode** is `platform` set AND `ledger: "verified"` — or, on a project that has not run `yad migrate` yet, `bridge_enabled` (or legacy `bridge`) `true`. `ledger` wins whenever it is present.
176
188
 
177
- **Bridge mode — do NOT write `state.json`.** The ledger is CI-owned: the `ledger-guard` check rejects
178
- any non-bot commit touching `epics/*/.sdlc/{state,approvals,comments,hub-prs}.json` or
189
+ **verified mode — do NOT write `state.json`.** The ledger is CI-owned: the `ledger-guard` check rejects
190
+ any non-bot commit touching `epics/*/.sdlc/{state,approvals,comments,product-prs,hub-prs}.json` or
179
191
  `epics/*/reviews/*.md`, `yad gate open` deliberately skips this write for the same reason, and
180
192
  `yad gate ci --merged` performs the whole transition when the review PR merges. Making the edit here
181
193
  fails the gate if it rides the review PR, and desynchronises the ledger CI is about to rewrite if it
@@ -183,18 +195,27 @@ is pushed around the gate. Commit the artifact set — **`ui-design.md`, `DESIGN
183
195
  design tool was used, `.sdlc/design-links.json`** (artifact-side, not ledger) — then hand off to
184
196
  `yad-review-gate`.
185
197
 
186
- **Otherwise — file-only, or a platform with no gate-sync CI — write it.** In `state.json`: set
187
- `ui-design.status: "done"`, set `ui-design-review.status: "in_review"`, and set
188
- `currentStep: "ui-design-review"`. Write `state.json`. Do **not** touch `approvals.json`. On this
189
- branch `yad gate open` makes the same edit, so it is a no-op once the gate has run.
198
+ **Otherwise — local, or a platform with no gate-sync CI — the engine makes this edit, not you.**
199
+ `yad gate open <epic> ui-design.md` marks `ui-design-review` `in_review`, closes `ui-design` as `done`, and moves `currentStep` to the gate
200
+ — unless the chain is already past it: a `ui-design` re-opened with a late `yad undefer` runs beside the
201
+ finished stories, and opening its review leaves `currentStep` where it is — the same transition, from the one function that owns it (`markInReview`, `cli/epic-state.mjs`).
202
+ `yad-review-gate action: open` runs that command; hand off to it rather than editing the ledger here.
203
+
204
+ **With no platform configured** it writes the ledger and simply opens no PR, so this works offline.
205
+ **With a platform** the `review/EP-<slug>/ui-design` branch must already be **on origin** — the command
206
+ refuses and writes nothing otherwise. Cut it from the authoring branch and push it before handing off
207
+ (or run `yad open-pr` from that branch: it pushes the branch, then delegates).
208
+
209
+ Do **not** hand-edit `state.json`, and do **not** touch `approvals.json` — only real reviewers approve,
210
+ through the gate.
190
211
 
191
212
  ### Step 6 — Stop at the gate (do NOT advance)
192
213
  Report: the paths to `ui-design.md` and `DESIGN.md`, whether Impeccable was used, the connected design
193
214
  tool and what it produced (e.g. "Figma — 4 screens generated", the file URL + `design-links.json` path,
194
215
  or "no design tool — markdown-only"), and that the next action is **review** via `yad-review-gate` (base
195
- rule: owner + 1 reviewer). **Never record approval here.** Front states do not auto-advance. When the hub has a platform, the gate opens a review PR on the
196
- hub (via `yad-hub-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments into
197
- the ledger; otherwise the review is recorded file-only.
216
+ rule: 1 distinct approver, who should not be the author). **Never record approval here.** Shape steps do not auto-advance. When the Product has a platform, the gate opens a review PR on the
217
+ Product (via `yad-hub-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments into
218
+ the ledger; otherwise the review is recorded local.
198
219
 
199
220
  ## Reference
200
221
  - Impeccable commands and the slash-command-vs-CLI deviation: `RESEARCH-NOTES.md` §4 + Deviation 3.
package/cli/roster.mjs DELETED
@@ -1,164 +0,0 @@
1
- // `yad roster` — manage the reviewer roster (.sdlc/hub.json) and per-repo roles at any time.
2
- // The roster maps a platform login -> SDLC name + a per-scope roles map; it is the only thing that lets
3
- // the review gate attribute approvals and route per-repo domain-owner reviewers. This is the standalone
4
- // counterpart of the `yad setup` roster step and the `yad-connect-repos action: roster` skill action.
5
- // Repo-driven: `add`/`edit` walks the connected repos from repos.json so roles are assigned against what
6
- // is actually connected, not against repo names the user has to remember. Granting/revoking a
7
- // `domain-owner` keeps repos.json `domain_owners` in sync so the gate never drifts from the roster.
8
- import path from 'node:path';
9
- import { c, log, ok, info, warn, hand, fail, ask, askYesNo, readJSON, writeJSON } from './lib.mjs';
10
- import { PROJECT_FILES } from './manifest.mjs';
11
- import { rolesForScope } from './platform.mjs';
12
- import { parseRolesSpec, upsertRosterEntry, addRepoRoles, removeRepoRole, setRepoDomainOwners, reconcileRepoRoles } from './setup.mjs';
13
-
14
- const ROLES = ['owner', 'reviewer', 'domain-owner'];
15
-
16
- const loadHub = (root) => readJSON(path.join(root, PROJECT_FILES.hubConfig), null);
17
- const loadRepos = (root) => readJSON(path.join(root, PROJECT_FILES.reposRegistry), { repos: [] }).repos || [];
18
- const ownersOf = (repo) => (Array.isArray(repo.domain_owners) ? repo.domain_owners : (repo.domain_owner ? [repo.domain_owner] : []));
19
-
20
- // `list` — every member with their per-scope roles, plus a drift check between hub.json roles and
21
- // repos.json domain_owners (the two should agree; the sync on grant/revoke keeps them aligned).
22
- function rosterList(root) {
23
- const hub = loadHub(root);
24
- if (!hub || !Array.isArray(hub.roster) || !hub.roster.length) {
25
- warn('no roster yet (.sdlc/hub.json) — add one with `yad roster add <login>` or `yad setup`');
26
- return { members: 0 };
27
- }
28
- const repos = loadRepos(root);
29
- log(c.bold('\nreviewer roster'));
30
- for (const e of hub.roster) {
31
- const flag = e.unverified ? c.yellow(' (unverified)') : '';
32
- log(` ${c.bold(e.name || e.login)} ${c.dim(`@${e.login}`)}${e.email ? c.dim(` <${e.email}>`) : ''}${flag}`);
33
- const hubRoles = rolesForScope(e, 'hub');
34
- log(` hub: ${hubRoles.length ? hubRoles.join(', ') : c.dim('—')}`);
35
- for (const r of repos) {
36
- const rr = rolesForScope(e, r.name);
37
- if (rr.length) log(` ${r.name}: ${rr.join(', ')}`);
38
- }
39
- }
40
- const drift = [];
41
- for (const r of repos) {
42
- const owners = ownersOf(r);
43
- for (const nm of owners) {
44
- const e = hub.roster.find((x) => x.name === nm);
45
- if (!e || !rolesForScope(e, r.name).includes('domain-owner')) {
46
- drift.push(`${nm} owns ${r.name} in repos.json but has no domain-owner role in hub.json`);
47
- }
48
- }
49
- for (const e of hub.roster) {
50
- if (rolesForScope(e, r.name).includes('domain-owner') && !owners.includes(e.name)) {
51
- drift.push(`${e.name} has domain-owner for ${r.name} in hub.json but is missing from repos.json domain_owners`);
52
- }
53
- }
54
- }
55
- if (drift.length) { log(''); for (const d of drift) warn(d); }
56
- return { members: hub.roster.length, repos: repos.length, drift: drift.length };
57
- }
58
-
59
- // The repo-driven walk: for each connected repo show the member's current role and offer to set it.
60
- async function repoWalk(root, entry) {
61
- const repos = loadRepos(root);
62
- if (!repos.length) { info('no connected repos to assign roles for (.sdlc/repos.json) — connect one with `yad setup`'); return; }
63
- for (const r of repos) {
64
- const cur = rolesForScope(entry, r.name);
65
- if (!(await askYesNo(` set ${entry.name}'s role on ${r.name}? (current: ${cur.length ? cur.join(', ') : 'none'})`, false))) continue;
66
- const input = await ask(' roles (domain-owner/reviewer/owner, space-separated; blank = clear)', cur.join(' '));
67
- const want = input.split(/\s+/).map((x) => x.trim()).filter(Boolean);
68
- const invalid = want.filter((x) => !ROLES.includes(x));
69
- if (invalid.length) warn(`ignoring unknown role(s): ${invalid.join(', ')} (allowed: ${ROLES.join(', ')})`);
70
- reconcileRepoRoles(root, entry.name, r.name, cur, want.filter((x) => ROLES.includes(x)));
71
- }
72
- }
73
-
74
- // Mirror any domain-owner scopes from a non-interactive `--roles` upsert into repos.json.
75
- function syncDomainOwners(root, entry) {
76
- for (const [scope, list] of Object.entries(entry.roles || {})) {
77
- if (scope !== 'hub' && Array.isArray(list) && list.includes('domain-owner')) setRepoDomainOwners(root, scope, entry.name, { add: true });
78
- }
79
- }
80
-
81
- // `add`/`edit <login>` — upsert by login (from flags or interactive prompts), then either apply a
82
- // `--roles` spec directly (scriptable) or run the repo-driven walk (interactive default).
83
- async function rosterAdd(root, login, { name, email, roles } = {}) {
84
- if (!login) { fail('usage: yad roster add <login> [--name N] [--email E] [--roles "hub=owner,reviewer backend=domain-owner"]'); process.exitCode = 1; return {}; }
85
- const hub = loadHub(root);
86
- const platform = hub ? hub.platform : null;
87
- const existing = hub && Array.isArray(hub.roster) ? hub.roster.find((e) => e.login === login) : null;
88
- const scripted = !!roles;
89
- let nm = name;
90
- let em = email;
91
- let rolesMap;
92
- if (scripted) {
93
- rolesMap = parseRolesSpec(roles);
94
- } else {
95
- nm = nm || await ask(' yad name', (existing && existing.name) || login);
96
- em = em || await ask(' commit email (blank to skip)', (existing && existing.email) || '');
97
- const def = rolesForScope(existing, 'hub').join(' ') || 'reviewer';
98
- const hubRoles = (await ask(' hub roles (owner/reviewer, space-separated)', def)).split(/\s+/).filter(Boolean);
99
- rolesMap = hubRoles.length ? { hub: hubRoles } : {};
100
- }
101
- const { entry, created } = upsertRosterEntry(root, { login, name: nm, email: em || undefined, roles: rolesMap, platform });
102
- if (!entry) return {};
103
- ok(`${created ? 'added' : 'updated'} ${entry.name} (@${login})`);
104
- if (scripted) syncDomainOwners(root, entry);
105
- else await repoWalk(root, entry);
106
- return { entry: entry.name, created };
107
- }
108
-
109
- // `grant <name> <repo> <role...>` — scriptable per-repo grant (member must already be in the roster).
110
- function rosterGrant(root, [name, repo, ...roles]) {
111
- if (!name || !repo || !roles.length) { fail('usage: yad roster grant <name> <repo> <role...>'); process.exitCode = 1; return {}; }
112
- const invalid = roles.filter((r) => !ROLES.includes(r));
113
- if (invalid.length) { fail(`unknown role(s): ${invalid.join(', ')} (allowed: ${ROLES.join(', ')})`); process.exitCode = 1; return {}; }
114
- const hub = loadHub(root);
115
- if (!hub || !Array.isArray(hub.roster) || !hub.roster.some((e) => e.name === name)) {
116
- fail(`'${name}' is not in the roster — add them first with \`yad roster add <login>\``); process.exitCode = 1; return {};
117
- }
118
- if (!loadRepos(root).some((r) => r.name === repo)) warn(`repo '${repo}' is not registered — the role is recorded and applies once it is connected`);
119
- addRepoRoles(root, repo, Object.fromEntries(roles.map((r) => [r, [name]])));
120
- if (roles.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: true });
121
- ok(`granted ${name} ${roles.join(', ')} on ${repo}`);
122
- return { name, repo, roles };
123
- }
124
-
125
- // `revoke <name> <repo> <role...>` — scriptable per-repo revoke.
126
- function rosterRevoke(root, [name, repo, ...roles]) {
127
- if (!name || !repo || !roles.length) { fail('usage: yad roster revoke <name> <repo> <role...>'); process.exitCode = 1; return {}; }
128
- removeRepoRole(root, name, repo, roles);
129
- if (roles.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: false });
130
- ok(`revoked ${name} ${roles.join(', ')} on ${repo}`);
131
- return { name, repo, roles };
132
- }
133
-
134
- // `remove <login>` — delete a member; warn (do not cascade) if still a domain owner in repos.json.
135
- function rosterRemove(root, login) {
136
- if (!login) { fail('usage: yad roster remove <login>'); process.exitCode = 1; return {}; }
137
- const hubPath = path.join(root, PROJECT_FILES.hubConfig);
138
- const hub = readJSON(hubPath, null);
139
- if (!hub || !Array.isArray(hub.roster)) { warn('no roster to remove from (.sdlc/hub.json)'); return { removed: 0 }; }
140
- const idx = hub.roster.findIndex((e) => e.login === login);
141
- if (idx < 0) { warn(`no roster member with login '${login}'`); return { removed: 0 }; }
142
- const [removed] = hub.roster.splice(idx, 1);
143
- writeJSON(hubPath, hub);
144
- ok(`removed ${removed.name} (@${login})`);
145
- const refs = loadRepos(root).filter((r) => ownersOf(r).includes(removed.name)).map((r) => r.name);
146
- if (refs.length) hand(`'${removed.name}' is still a domain owner in repos.json for: ${refs.join(', ')} — revoke with \`yad roster revoke ${removed.name} <repo> domain-owner\``);
147
- return { removed: 1 };
148
- }
149
-
150
- export async function runRoster(root, { action = 'list', args = [], name, email, roles } = {}) {
151
- switch (action) {
152
- case 'list': return rosterList(root);
153
- case 'add':
154
- case 'edit': return rosterAdd(root, args[0], { name, email, roles });
155
- case 'grant': return rosterGrant(root, args);
156
- case 'revoke': return rosterRevoke(root, args);
157
- case 'remove':
158
- case 'rm': return rosterRemove(root, args[0]);
159
- default:
160
- fail(`unknown roster action: ${action} (list | add | edit | grant | revoke | remove)`);
161
- process.exitCode = 1;
162
- return {};
163
- }
164
- }