yadflow 2.1.0 → 2.3.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.
package/CHANGELOG.md CHANGED
@@ -1,15 +1,9 @@
1
- # [2.1.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.0.1...v2.1.0) (2026-06-13)
2
-
3
-
4
- ### Bug Fixes
5
-
6
- * address CodeRabbit review on the hardening PR ([7dbe9e3](https://github.com/abdelrahmannasr/yadflow/commit/7dbe9e358e731d69ecead6ecac9faa8377c37023))
7
- * drop useless backtick escapes in a single-quoted doctor hint (lint) ([c0cf1a2](https://github.com/abdelrahmannasr/yadflow/commit/c0cf1a26c15fccc91df15013d6bac83892f2af25))
1
+ # [2.3.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.2.0...v2.3.0) (2026-06-13)
8
2
 
9
3
 
10
4
  ### Features
11
5
 
12
- * yad doctor + structured YAD-* error codes with recovery hints ([94f9e9f](https://github.com/abdelrahmannasr/yadflow/commit/94f9e9f6ff6d6d3c83ed29f1cfcc97e32678615c))
6
+ * add parallel test-cases step with pluggable testing-tool connection ([#45](https://github.com/abdelrahmannasr/yadflow/issues/45)) ([19c282f](https://github.com/abdelrahmannasr/yadflow/commit/19c282f6bd737364bca122179b05de8ea94493a9))
13
7
 
14
8
  # [1.1.0](https://github.com/abdelrahmannasr/sdlc-workflow/compare/v1.0.3...v1.1.0) (2026-06-09)
15
9
 
package/README.md CHANGED
@@ -49,8 +49,9 @@ human**. Detailed walkthroughs for each phase follow below.
49
49
  | `skills/yad-architecture/` | Front state 3: author `architecture.md` + the locked `contract.md`; hash-lock the contract surface. |
50
50
  | `skills/yad-ui/` | Front state 5: author `ui-design.md` + `DESIGN.md` (Impeccable slash-commands, or graceful fallback). |
51
51
  | `skills/yad-stories/` | Front state 7: break the epic into repo-tagged stories with stable `EP-<slug>-S0N` IDs. |
52
+ | `skills/yad-test-cases/` | Front state 9: with the test architect author `test-cases.md`; implement the automation in the connected testing tool, or produce artifacts only. |
52
53
  | `skills/yad-connect-repos/` | Connect code repos to the hub (GitHub/GitLab, local-user auth); cache a Repomix pack + **code-map** per repo so the front phases are code-aware. |
53
- | `skills/yad-review-gate/` | The reusable **team review + approve gate** (used for all four reviews). |
54
+ | `skills/yad-review-gate/` | The reusable **team review + approve gate** (used for all five reviews). |
54
55
  | `skills/yad-spec/` | Build Step A: run the Spec Kit ceremony once per story per repo → `specs/<story-id>/`. |
55
56
  | `skills/yad-implement/` | Build Step B: implement ONE atomic task as a small diff on its own branch. |
56
57
  | `skills/yad-checks/` | Build Step C: wire + run the CI gates (spec-link, contract-check, build/test/lint, verified-commits). |
@@ -122,16 +123,21 @@ a manual `yad gate sync` racing CI, or GitLab pipelines — two simultaneous syn
122
123
  *commits* via the rebase retry but each works from the state it read at start, so the rarer of two
123
124
  simultaneous advancements can be lost; the next event or scheduled sweep re-syncs and converges.
124
125
 
125
- ### What `setup` walks you through (7 steps)
126
+ ### What `setup` walks you through (9 steps)
126
127
 
127
128
  1. **Preflight** — confirm the hub is a git repo (offers `git init`); check `git`/`node`/`npx`.
128
- 2. **Install the module** — copy all 17 `yad-*` skills into the IDE skill dirs you pick
129
+ 2. **Install the module** — copy all 20 `yad-*` skills into the IDE skill dirs you pick
129
130
  (`.claude/`, `.agents/`, `.zencoder/`, `.opencode/`) and register `_bmad/sdlc/`.
130
131
  3. **Hub platform & roster** — detect GitHub/GitLab from the remote; record reviewers → `.sdlc/hub.json`.
131
- 4. **Connect code repos** — register each repo into `.sdlc/repos.json` and cache a Repomix pack.
132
- 5. **Wire each repo** CI gates, PR/MR template, and review-comment scaffold.
133
- 6. **AI review** — optionally write `.coderabbit.yaml`.
134
- 7. **Done** — stamp `.sdlc/cli-version.json` and hand off the AI-only steps (code-maps; first epic).
132
+ 4. **Connect a design tool** — record the design tool (Figma / pencil / none) → `.sdlc/design.json` so
133
+ the UI step can materialize the design; the MCP itself is confirmed later by `yad-connect-design`.
134
+ 5. **Connect a testing tool** — record the testing tool (Playwright / cypress / pytest / none) →
135
+ `.sdlc/testing.json` so the test-cases step can implement the automation; the MCP itself is confirmed
136
+ later by `yad-connect-testing`.
137
+ 6. **Connect code repos** — register each repo into `.sdlc/repos.json` and cache a Repomix pack.
138
+ 7. **Wire each repo** — CI gates, PR/MR template, and review-comment scaffold.
139
+ 8. **AI review** — optionally write `.coderabbit.yaml`.
140
+ 9. **Done** — stamp `.sdlc/cli-version.json` and hand off the AI-only steps (code-maps; first epic).
135
141
 
136
142
  The deterministic file work runs automatically; the AI-only steps are handed to the Claude Code skills
137
143
  with a printed next-action. Re-run `… check --fix` any time the workflow updates — it never re-asks for
@@ -167,10 +173,12 @@ with a fix-it hint per finding. Failures carry stable, greppable codes, also pri
167
173
  | `YAD-STATE-002` | a ledger/config file parses but has the wrong shape | fix the file or restore from git (the message names the field) |
168
174
  | `YAD-STATE-003` | a registered repo path is missing or not a git repo | fix the path in `.sdlc/repos.json` or re-connect the repo |
169
175
  | `YAD-CFG-001` | `hub.json` names an unknown platform | expected `github`, `gitlab`, or `null` — fix it or re-run `yad setup` |
176
+ | `YAD-CFG-002` | `design.json` names an unknown design tool | expected one of `config.yaml` `design.tools` (e.g. `figma`, `pencil`), or `none` — fix it or re-run `yad setup` |
177
+ | `YAD-CFG-003` | `testing.json` names an unknown testing tool | expected one of `config.yaml` `testing.tools` (e.g. `playwright`, `cypress`, `pytest`), or `none` — fix it or re-run `yad setup` |
170
178
 
171
179
  Filing a bug? Attach `yad doctor --json` — it contains no secrets (names, paths, and check results only).
172
180
 
173
- ## Agent skills (all 17)
181
+ ## Agent skills (all 20)
174
182
 
175
183
  The CLI **installs and wires** the module; the skills below are the **agents you invoke by name** in your
176
184
  AI IDE (e.g. *“run `yad-epic`”*) to actually do the work. State lives in files you can also edit
@@ -183,12 +191,22 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
183
191
  `.sdlc/repos.json`, then caches an AI-readable picture of each — a compressed Repomix pack and a
184
192
  lightweight code-map (existing endpoints/events/data-models/modules), secret-scanned. Idempotent and
185
193
  refreshable; staleness tracked by HEAD sha.
194
+ - **`yad-connect-design`** — Connects a design tool (Figma-first, pluggable) so the UI step can
195
+ materialize the actual feature design (mobile screens / web pages) inside it, alongside the Markdown.
196
+ Records the tool + project/file references in `.sdlc/design.json` (local-user / MCP-session auth, no
197
+ stored tokens), detecting the design-tool MCP and degrading to markdown-only when absent. Idempotent
198
+ and refreshable; one connection per project.
199
+ - **`yad-connect-testing`** — Connects a testing tool (Playwright-first, pluggable) so the test-cases
200
+ step can implement the actual automation tests inside it, alongside the Markdown. Records the tool +
201
+ project/suite references in `.sdlc/testing.json` (local-user / MCP-session auth, no stored tokens),
202
+ detecting the testing-tool MCP and degrading to artifacts-only when absent. Idempotent and
203
+ refreshable; one connection per project.
186
204
 
187
205
  ### Front half — author the "thinking" (once per epic, human-gated)
188
206
 
189
207
  - **`yad-analysis`** — *Optional* front state 1. With the analyst, pressure-test a feature idea
190
208
  and write the discovery brief into `analysis.md`. Assigns the `EP-<slug>` ID and seeds `.sdlc/` state
191
- (the 10-step chain that puts analysis before epic). If skipped, the epic step does this shaping inline.
209
+ (the 12-step chain that puts analysis before epic). If skipped, the epic step does this shaping inline.
192
210
  - **`yad-epic`** — The epic front state. Shape the idea with the analyst (or read `analysis.md`
193
211
  when it already ran), then write the epic with the pm into `epic.md`. The entry point when analysis is
194
212
  skipped: assigns the `EP-<slug>` ID and seeds `.sdlc/` state.
@@ -197,10 +215,19 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
197
215
  `.sdlc/contract-lock.json`. Reads `epic.md`; escalates on the contract risk tag.
198
216
  - **`yad-ui`** — Front state 5. With the ux-designer, author `ui-design.md` and `DESIGN.md`,
199
217
  driving Impeccable as harness slash-commands (document/extract/craft) when installed, or authoring
200
- directly when not. Reads epic + architecture.
218
+ directly when not. When a design tool is connected (`yad-connect-design`), also **materializes the
219
+ feature design** — mobile screens / web pages — in the tool (generate or link), recording the
220
+ screen→frame map in `design-links.json`; degrades to markdown-only otherwise. Reads epic + architecture.
201
221
  - **`yad-stories`** — Front state 7. With the pm, break the approved epic into user stories, each
202
222
  tagged with the repos that must implement it. Assigns zero-padded `EP-<slug>-S0N` IDs, one file per
203
223
  story under `stories/`. Reads epic + architecture + contract + UI.
224
+ - **`yad-test-cases`** — Front state 9, a **parallel, non-blocking** track: it opens when the stories
225
+ gate passes (the epic is already `ready-for-build`, so the build half runs alongside it). With the
226
+ test architect (Murat), author `test-cases.md` covering the approved stories (risk-based P0–P3 cases +
227
+ story→case traceability). When a testing tool is connected (`yad-connect-testing`), also **implements
228
+ the automation tests** in the connected code repo(s) (generate or link), recording the case→test map in
229
+ `test-links.json`; degrades to artifacts-only otherwise. Reads epic + architecture + contract + UI +
230
+ stories.
204
231
 
205
232
  ### The review gate (cross-cutting — used by every review)
206
233
 
@@ -259,7 +286,7 @@ merging the approved, fully-resolved review PR — never on a machine.
259
286
 
260
287
  As of **Phase 4a** the `automation` dial is no longer inert: the orchestrator `yad-run` reads it and,
261
288
  for the safe **back** steps, advances on its own when a step is set to `machine_advance` (and has
262
- *earned* it — see "Run the back half on the dial" below). The engineer review and all four front
289
+ *earned* it — see "Run the back half on the dial" below). The engineer review and all five front
263
290
  states stay `human_approve` forever.
264
291
 
265
292
  ## Using the workflow end to end (all the steps, in order)
@@ -310,7 +337,10 @@ threads are resolved. Details: **“Run the full front half by hand”** below.
310
337
  7. `yad-architecture` → `architecture.md` + locked `contract.md` → review (**escalated**: contract).
311
338
  8. `yad-ui` → `ui-design.md` + `DESIGN.md` → review (base rule).
312
339
  9. `yad-stories` → repo-tagged `stories/EP-<slug>-S0N.md` → review (**per-repo**).
313
- → `state.json` reaches `currentStep: ready-for-build`.
340
+ → `state.json` reaches `currentStep: ready-for-build` — **the build half can start now.**
341
+ 10. `yad-test-cases` → `test-cases.md` (+ automation tests when a testing tool is connected) → review (base rule).
342
+ **Parallel, non-blocking:** opens when the stories gate passes and runs alongside the build half; its
343
+ review never moves `currentStep` off `ready-for-build`.
314
344
 
315
345
  ### B — Build half (per story, per repo)
316
346
  From a `ready-for-build` story, for **each** repo the story is tagged with. Details: **“Run the full
@@ -345,12 +375,13 @@ Details: **“Run the back half on the dial”** below.
345
375
  ## Run the full front half by hand
346
376
 
347
377
  The front half walks **epic → review → architecture+contract → review → UI design → review → stories
348
- → review → `ready-for-build`**. It is all files under `epics/EP-<slug>/`. The skills below guide you,
349
- but you can also edit the files directly that's the point.
378
+ → review → `ready-for-build`**, then **test cases review** runs as a **parallel, non-blocking track**
379
+ alongside the build half. It is all files under `epics/EP-<slug>/`. The skills below guide you, but you
380
+ can also edit the files directly — that's the point.
350
381
 
351
382
  Each authoring step is the same shape: an author skill produces an artifact, sets its step `done`,
352
383
  moves `currentStep` to the matching review, and **stops at the gate**. Then **`yad-review-gate`**
353
- (one gate, reused for all four reviews) takes `open → comment → approve → advance`. When the hub is on a
384
+ (one gate, reused for all five reviews) takes `open → comment → approve → advance`. When the hub is on a
354
385
  platform, the **`yad gate`** CLI runs that gate over a real PR/MR — `open` raises the review PR, `sync`
355
386
  pulls approvals + comment threads into the ledger, and the step **auto-advances when the approved,
356
387
  fully-resolved PR is merged** (the merge is the human approval act).
@@ -407,7 +438,7 @@ accumulate, and the step moves forward only when the rule is met. **File-only**
407
438
  in any story's `repos`**.
408
439
 
409
440
  ### Check status anytime
410
- Invoke **`yad-status`** (read-only) to see the full 8-step chain, every step's dials/status, the
441
+ Invoke **`yad-status`** (read-only) to see the full 10-step chain, every step's dials/status, the
411
442
  contract lock, story repo tags, and which approvals the active gate still needs.
412
443
 
413
444
  ## Worked example (already in this repo)
package/cli/doctor.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  import path from 'node:path';
6
6
  import fs from 'node:fs';
7
7
  import { c, log, ok, info, warn, fail, hand, run, has, exists, readJSON, readJSONStrict } from './lib.mjs';
8
- import { VERSION, PROJECT_FILES } from './manifest.mjs';
8
+ import { VERSION, PROJECT_FILES, DESIGN_TOOLS, TESTING_TOOLS } from './manifest.mjs';
9
9
  import { loadLedger, epicRoot } from './epic-state.mjs';
10
10
  import { gitHead } from './setup.mjs';
11
11
  import { cliFor } from './platform.mjs';
@@ -75,6 +75,46 @@ export function projectChecks(checks, root) {
75
75
  }
76
76
  }
77
77
 
78
+ // design.json: parse + shape + tool + MCP confirmation (absent is the normal markdown-only default —
79
+ // pre-feature projects have none, so silence rather than warn when the file does not exist).
80
+ const designPath = path.join(root, PROJECT_FILES.designConfig);
81
+ if (exists(designPath)) {
82
+ let design = null, designBroken = false;
83
+ try {
84
+ design = readJSONStrict(designPath, null);
85
+ } catch (e) {
86
+ designBroken = true;
87
+ check(checks, 'design', 'project', 'fail', `${PROJECT_FILES.designConfig} does not parse [${e.code || 'YAD-STATE-001'}]`, e.hint || 'fix the JSON or restore it from git');
88
+ }
89
+ if (designBroken) { /* reported above */ }
90
+ else if (typeof design !== 'object' || Array.isArray(design) || design === null) check(checks, 'design', 'project', 'fail', `${PROJECT_FILES.designConfig} has the wrong shape [YAD-STATE-002]`, 'expected a JSON object');
91
+ else if (design.tool === 'none') check(checks, 'design', 'project', 'ok', 'design: markdown-only');
92
+ else if (!DESIGN_TOOLS.includes(design.tool)) check(checks, 'design', 'project', 'fail', `${PROJECT_FILES.designConfig}: unknown or missing design tool '${design.tool}' [YAD-CFG-002]`, `expected one of ${DESIGN_TOOLS.join(', ')}, or none`);
93
+ else if (design.source && design.source !== 'unavailable') check(checks, 'design', 'project', 'ok', `design: ${design.tool} (${design.source})`);
94
+ else if (design.source === 'unavailable') check(checks, 'design', 'project', 'warn', `design: ${design.tool} MCP unavailable — yad-ui runs markdown-only`, 'connect the MCP, then run `yad-connect-design` (action: refresh)');
95
+ else check(checks, 'design', 'project', 'warn', `design: ${design.tool} recorded but the MCP is not confirmed`, 'run `yad-connect-design` in Claude Code to detect the MCP');
96
+ }
97
+
98
+ // testing.json: parse + shape + tool + MCP confirmation (absent is the normal artifacts-only default —
99
+ // pre-feature projects have none, so silence rather than warn when the file does not exist).
100
+ const testingPath = path.join(root, PROJECT_FILES.testingConfig);
101
+ if (exists(testingPath)) {
102
+ let testing = null, testingBroken = false;
103
+ try {
104
+ testing = readJSONStrict(testingPath, null);
105
+ } catch (e) {
106
+ testingBroken = true;
107
+ check(checks, 'testing', 'project', 'fail', `${PROJECT_FILES.testingConfig} does not parse [${e.code || 'YAD-STATE-001'}]`, e.hint || 'fix the JSON or restore it from git');
108
+ }
109
+ if (testingBroken) { /* reported above */ }
110
+ else if (typeof testing !== 'object' || Array.isArray(testing) || testing === null) check(checks, 'testing', 'project', 'fail', `${PROJECT_FILES.testingConfig} has the wrong shape [YAD-STATE-002]`, 'expected a JSON object');
111
+ else if (testing.tool === 'none') check(checks, 'testing', 'project', 'ok', 'testing: artifacts-only');
112
+ else if (!TESTING_TOOLS.includes(testing.tool)) check(checks, 'testing', 'project', 'fail', `${PROJECT_FILES.testingConfig}: unknown or missing testing tool '${testing.tool}' [YAD-CFG-003]`, `expected one of ${TESTING_TOOLS.join(', ')}, or none`);
113
+ else if (testing.source && testing.source !== 'unavailable') check(checks, 'testing', 'project', 'ok', `testing: ${testing.tool} (${testing.source})`);
114
+ else if (testing.source === 'unavailable') check(checks, 'testing', 'project', 'warn', `testing: ${testing.tool} MCP unavailable — yad-test-cases runs artifacts-only`, 'connect the MCP, then run `yad-connect-testing` (action: refresh)');
115
+ else check(checks, 'testing', 'project', 'warn', `testing: ${testing.tool} recorded but the MCP is not confirmed`, 'run `yad-connect-testing` in Claude Code to detect the MCP');
116
+ }
117
+
78
118
  // repos.json: parse + every entry is a live git repo; staleness vs syncedHead
79
119
  let registry = { repos: [] };
80
120
  let regBroken = false;
@@ -197,9 +197,24 @@ export function gatePredicate({
197
197
 
198
198
  // Advance the step in state.json once the predicate passes. Mirrors yad-review-gate Step 3:
199
199
  // mark this review step done, unblock the next step, or set `ready-for-build` for the last one.
200
+ //
201
+ // `test-cases` is a PARALLEL, non-blocking track so the build half can start while the tester works:
202
+ // approving `stories-review` makes the epic `ready-for-build` (the build half keys off this) AND opens
203
+ // `test-cases` for the tester; completing `test-cases-review` never pulls `currentStep` back from
204
+ // `ready-for-build`. Both rules degrade safely for an old chain that has no test-cases steps.
200
205
  export function advanceState(state, step) {
201
206
  const i = state.steps.findIndex((s) => s.id === step.id);
202
207
  state.steps[i] = { ...state.steps[i], status: 'done' };
208
+ if (step.id === 'stories-review') {
209
+ const tc = state.steps.find((s) => s.id === 'test-cases');
210
+ if (tc && tc.status === 'blocked') tc.status = 'in_progress';
211
+ state.currentStep = 'ready-for-build';
212
+ return state;
213
+ }
214
+ if (step.id === 'test-cases-review') {
215
+ state.currentStep = 'ready-for-build';
216
+ return state;
217
+ }
203
218
  const next = state.steps[i + 1];
204
219
  if (next) {
205
220
  next.status = next.type === 'review+approve' ? 'in_review' : 'in_progress';
@@ -210,11 +225,13 @@ export function advanceState(state, step) {
210
225
  return state;
211
226
  }
212
227
 
213
- // Mark a step in-review (idempotent) and point currentStep at it.
228
+ // Mark a step in-review (idempotent) and point currentStep at it — EXCEPT once the epic is
229
+ // `ready-for-build`: the parallel `test-cases` track must not pull currentStep back (the build half
230
+ // runs alongside the tester, and only the test-cases review is in flight at that point).
214
231
  export function markInReview(state, step) {
215
232
  const i = state.steps.findIndex((s) => s.id === step.id);
216
233
  if (state.steps[i].status !== 'done') state.steps[i].status = 'in_review';
217
- state.currentStep = step.id;
234
+ if (state.currentStep !== 'ready-for-build') state.currentStep = step.id;
218
235
  return state;
219
236
  }
220
237
 
package/cli/errors.mjs CHANGED
@@ -20,6 +20,8 @@ export const CODES = {
20
20
  'YAD-STATE-002': 'a ledger/config JSON file parses but has the wrong shape',
21
21
  'YAD-STATE-003': 'a registered repo path is missing or not a git repository',
22
22
  'YAD-CFG-001': 'hub.json names an unknown platform (expected github, gitlab, or null)',
23
+ 'YAD-CFG-002': 'design.json names an unknown design tool (expected one of config.yaml design.tools, or none)',
24
+ 'YAD-CFG-003': 'testing.json names an unknown testing tool (expected one of config.yaml testing.tools, or none)',
23
25
  };
24
26
 
25
27
  export const err = (code, message, hint) => new YadError(code, message, hint);
package/cli/manifest.mjs CHANGED
@@ -10,14 +10,17 @@ import { readFileSync } from 'node:fs';
10
10
  const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
11
11
  export const VERSION = version;
12
12
 
13
- // The 17 hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
13
+ // The 20 hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
14
14
  export const SKILLS = [
15
15
  'yad-analysis',
16
16
  'yad-epic',
17
17
  'yad-architecture',
18
18
  'yad-ui',
19
19
  'yad-stories',
20
+ 'yad-test-cases',
20
21
  'yad-connect-repos',
22
+ 'yad-connect-design',
23
+ 'yad-connect-testing',
21
24
  'yad-spec',
22
25
  'yad-implement',
23
26
  'yad-checks',
@@ -79,10 +82,26 @@ export const IDE_OPENCODE_DIR = '.opencode/commands'; // <skill>.md (flat SKILL.
79
82
  // Module registration files copied from skills/sdlc/ into _bmad/sdlc/.
80
83
  export const MODULE_FILES = ['config.yaml', 'module-help.csv'];
81
84
 
85
+ // Supported design-tool adapters (mirrors skills/sdlc/config.yaml `design.tools`); `DESIGN_PRIMARY` is
86
+ // the fallback `registerDesign`/setup use when an unknown tool is named, and `none` is the explicit
87
+ // markdown-only choice. (doctor does NOT fall back — an unknown tool there is a hard YAD-CFG-002 fail,
88
+ // mirroring how registerRepo falls back on platform while doctor fails on an unknown hub platform.)
89
+ export const DESIGN_TOOLS = ['figma', 'pencil'];
90
+ export const DESIGN_PRIMARY = 'figma';
91
+
92
+ // Supported testing-tool adapters (mirrors skills/sdlc/config.yaml `testing.tools`); `TESTING_PRIMARY`
93
+ // is the fallback `registerTesting`/setup use when an unknown tool is named, and `none` is the explicit
94
+ // artifacts-only choice. (doctor does NOT fall back — an unknown tool there is a hard YAD-CFG-003 fail,
95
+ // mirroring the design-tool YAD-CFG-002.)
96
+ export const TESTING_TOOLS = ['playwright', 'cypress', 'pytest'];
97
+ export const TESTING_PRIMARY = 'playwright';
98
+
82
99
  // Project-level files setup produces (used by `check` to spot missing setup).
83
100
  export const PROJECT_FILES = {
84
101
  reposRegistry: '.sdlc/repos.json',
85
102
  hubConfig: '.sdlc/hub.json',
103
+ designConfig: '.sdlc/design.json',
104
+ testingConfig: '.sdlc/testing.json',
86
105
  version: '.sdlc/cli-version.json',
87
106
  };
88
107
 
package/cli/setup.mjs CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  c, log, step, ok, info, warn, hand, fail, ask, askYesNo, run, has,
6
6
  exists, readJSON, writeJSON,
7
7
  } from './lib.mjs';
8
- import { VERSION, IDE_FOLDER_TARGETS, PROJECT_FILES } from './manifest.mjs';
8
+ import { VERSION, IDE_FOLDER_TARGETS, PROJECT_FILES, DESIGN_TOOLS, DESIGN_PRIMARY, TESTING_TOOLS, TESTING_PRIMARY } from './manifest.mjs';
9
9
  import { moduleActions, repoActions, hubActions, authorsActions } from './plan.mjs';
10
10
 
11
11
  const ALL_IDES = [...IDE_FOLDER_TARGETS, '.opencode'];
@@ -58,6 +58,63 @@ export function registerRepo(root, registry, { name, rpath, platform, domain_own
58
58
  return repo;
59
59
  }
60
60
 
61
+ // Record the project's design-tool connection into .sdlc/design.json (the deterministic half of the
62
+ // connect loop; MCP detection itself is an AI step, handed off to `yad-connect-design`). An unknown tool
63
+ // falls back to the primary adapter rather than being rejected — mirrors registerRepo's platform
64
+ // fallback and the hub step. `none` is the explicit markdown-only choice.
65
+ export function registerDesign(root, { tool, project_url = null, files = null, today = null } = {}) {
66
+ // Idempotent re-connect: carry the original first-connect date forward (the schema defines
67
+ // connectedAt as "first connect"); only lastSyncedAt moves. Mirrors repo.mjs refresh.
68
+ const designPath = path.join(root, PROJECT_FILES.designConfig);
69
+ const prev = readJSON(designPath, null);
70
+ const connectedAt = prev && prev.connectedAt ? prev.connectedAt : today;
71
+ let t = (tool || '').toLowerCase();
72
+ if (t === 'none' || t === '') {
73
+ const off = { tool: 'none', provider: null, project_url: null, auth: 'user',
74
+ files: { web: null, mobile: null }, connectedAt, lastSyncedAt: today, source: 'unavailable' };
75
+ writeJSON(designPath, off);
76
+ return off;
77
+ }
78
+ if (!DESIGN_TOOLS.includes(t)) { warn(`unknown design tool '${tool}' — using ${DESIGN_PRIMARY}`); t = DESIGN_PRIMARY; }
79
+ // source stays null until `yad-connect-design` detects the MCP in the harness (AI step). doctor reports
80
+ // a recorded-but-unconfirmed connection as a warning pointing at that skill.
81
+ const design = {
82
+ tool: t, provider: null, project_url: project_url || null, auth: 'user',
83
+ files: files || { web: null, mobile: null },
84
+ connectedAt, lastSyncedAt: today, source: null,
85
+ };
86
+ writeJSON(designPath, design);
87
+ return design;
88
+ }
89
+
90
+ // Record the project's testing-tool connection into .sdlc/testing.json (the deterministic half of the
91
+ // connect loop; MCP detection itself is an AI step, handed off to `yad-connect-testing`). An unknown
92
+ // tool falls back to the primary adapter rather than being rejected — mirrors registerDesign. `none` is
93
+ // the explicit artifacts-only choice.
94
+ export function registerTesting(root, { tool, project_url = null, suites = null, today = null } = {}) {
95
+ // Idempotent re-connect: carry the original first-connect date forward; only lastSyncedAt moves.
96
+ const testingPath = path.join(root, PROJECT_FILES.testingConfig);
97
+ const prev = readJSON(testingPath, null);
98
+ const connectedAt = prev && prev.connectedAt ? prev.connectedAt : today;
99
+ let t = (tool || '').toLowerCase();
100
+ if (t === 'none' || t === '') {
101
+ const off = { tool: 'none', provider: null, project_url: null, auth: 'user',
102
+ suites: {}, connectedAt, lastSyncedAt: today, source: 'unavailable' };
103
+ writeJSON(testingPath, off);
104
+ return off;
105
+ }
106
+ if (!TESTING_TOOLS.includes(t)) { warn(`unknown testing tool '${tool}' — using ${TESTING_PRIMARY}`); t = TESTING_PRIMARY; }
107
+ // source stays null until `yad-connect-testing` detects the MCP in the harness (AI step). doctor
108
+ // reports a recorded-but-unconfirmed connection as a warning pointing at that skill.
109
+ const testing = {
110
+ tool: t, provider: null, project_url: project_url || null, auth: 'user',
111
+ suites: suites || {},
112
+ connectedAt, lastSyncedAt: today, source: null,
113
+ };
114
+ writeJSON(testingPath, testing);
115
+ return testing;
116
+ }
117
+
61
118
  function applyActions(actions, { force = false } = {}) {
62
119
  let changed = 0;
63
120
  for (const a of actions) {
@@ -71,7 +128,7 @@ function applyActions(actions, { force = false } = {}) {
71
128
  }
72
129
 
73
130
  export async function runSetup(root, opts = {}) {
74
- const total = 7;
131
+ const total = 9;
75
132
  log(c.bold(`\nSDLC Workflow setup ${c.dim('v' + VERSION)}`));
76
133
  log(c.dim(`target: ${root}`));
77
134
 
@@ -131,8 +188,44 @@ export async function runSetup(root, opts = {}) {
131
188
  ok(`wrote ${PROJECT_FILES.hubConfig} (${roster.length} reviewer(s))`);
132
189
  }
133
190
 
134
- // 4. Connect code repos
135
- step(4, total, 'Connect code repos');
191
+ // 4. Connect a design tool (Figma-first, pluggable; the UI step materializes the design here)
192
+ step(4, total, 'Connect a design tool (Figma / pencil / none)');
193
+ const designPath = path.join(root, PROJECT_FILES.designConfig);
194
+ if (exists(designPath) && !(await askYesNo('design.json exists — reconfigure?', false))) {
195
+ info('keeping existing .sdlc/design.json');
196
+ } else {
197
+ let tool = (await ask(`Design tool (${DESIGN_TOOLS.join('/')}/none)`, DESIGN_PRIMARY)).toLowerCase();
198
+ if (![...DESIGN_TOOLS, 'none'].includes(tool)) {
199
+ warn(`unknown design tool '${tool}' — using ${DESIGN_PRIMARY}`);
200
+ tool = DESIGN_PRIMARY;
201
+ }
202
+ const project_url = tool === 'none' ? null : (await ask(' project/file URL (blank to set later)', '')) || null;
203
+ registerDesign(root, { tool, project_url, today: opts.today ?? null });
204
+ ok(tool === 'none'
205
+ ? `wrote ${PROJECT_FILES.designConfig} (markdown-only)`
206
+ : `wrote ${PROJECT_FILES.designConfig} (${tool})`);
207
+ }
208
+
209
+ // 5. Connect a testing tool (Playwright-first, pluggable; the test-cases step implements automation here)
210
+ step(5, total, 'Connect a testing tool (playwright / cypress / pytest / none)');
211
+ const testingPath = path.join(root, PROJECT_FILES.testingConfig);
212
+ if (exists(testingPath) && !(await askYesNo('testing.json exists — reconfigure?', false))) {
213
+ info('keeping existing .sdlc/testing.json');
214
+ } else {
215
+ let tool = (await ask(`Testing tool (${TESTING_TOOLS.join('/')}/none)`, TESTING_PRIMARY)).toLowerCase();
216
+ if (![...TESTING_TOOLS, 'none'].includes(tool)) {
217
+ warn(`unknown testing tool '${tool}' — using ${TESTING_PRIMARY}`);
218
+ tool = TESTING_PRIMARY;
219
+ }
220
+ const project_url = tool === 'none' ? null : (await ask(' project/config reference (blank to set later)', '')) || null;
221
+ registerTesting(root, { tool, project_url, today: opts.today ?? null });
222
+ ok(tool === 'none'
223
+ ? `wrote ${PROJECT_FILES.testingConfig} (artifacts-only)`
224
+ : `wrote ${PROJECT_FILES.testingConfig} (${tool})`);
225
+ }
226
+
227
+ // 6. Connect code repos
228
+ step(6, total, 'Connect code repos');
136
229
  const regPath = path.join(root, PROJECT_FILES.reposRegistry);
137
230
  const registry = readJSON(regPath, { repos: [] });
138
231
  const known = new Set(registry.repos.map((r) => r.name));
@@ -155,8 +248,8 @@ export async function runSetup(root, opts = {}) {
155
248
  }
156
249
  }
157
250
 
158
- // 5. Wire each connected repo + the hub itself
159
- step(5, total, 'Wire connected repos + the hub (CI gates, PR template, comment scaffold, gate-sync)');
251
+ // 7. Wire each connected repo + the hub itself
252
+ step(7, total, 'Wire connected repos + the hub (CI gates, PR template, comment scaffold, gate-sync)');
160
253
  if (registry.repos.length === 0) info('no repos to wire');
161
254
  for (const repo of registry.repos) {
162
255
  log(` ${c.bold(repo.name)} ${c.dim(`(${repo.platform})`)}`);
@@ -171,8 +264,8 @@ export async function runSetup(root, opts = {}) {
171
264
  // author allowlists for the verified-commits gate (hub + every repo), from the roster emails
172
265
  applyActions(authorsActions(root, registry.repos), { force: true });
173
266
 
174
- // 6. Optional CodeRabbit
175
- step(6, total, 'AI review (CodeRabbit)');
267
+ // 8. Optional CodeRabbit
268
+ step(8, total, 'AI review (CodeRabbit)');
176
269
  for (const repo of registry.repos) {
177
270
  const cr = path.join(path.resolve(root, repo.path), '.coderabbit.yaml');
178
271
  if (exists(cr)) { info(`${repo.name}: .coderabbit.yaml present`); continue; }
@@ -182,13 +275,21 @@ export async function runSetup(root, opts = {}) {
182
275
  }
183
276
  }
184
277
 
185
- // 7. Summary + version stamp
186
- step(7, total, 'Done');
278
+ // 9. Summary + version stamp
279
+ step(9, total, 'Done');
187
280
  writeJSON(path.join(root, PROJECT_FILES.version), { version: VERSION, ideTargets, updatedAt: opts.today ?? null });
188
281
  ok(`stamped ${PROJECT_FILES.version} (v${VERSION})`);
189
282
  log('');
190
283
  log(c.bold('Next — AI-only steps (run in Claude Code):'));
191
284
  hand('generate code-maps: run `yad-connect-repos` for each connected repo');
285
+ const design = readJSON(designPath, null);
286
+ if (design && design.tool && design.tool !== 'none') {
287
+ hand(`confirm the design tool: run \`yad-connect-design\` to detect the ${design.tool} MCP (or it degrades to markdown-only)`);
288
+ }
289
+ const testing = readJSON(testingPath, null);
290
+ if (testing && testing.tool && testing.tool !== 'none') {
291
+ hand(`confirm the testing tool: run \`yad-connect-testing\` to detect the ${testing.tool} MCP (or it degrades to artifacts-only)`);
292
+ }
192
293
  hand('author your first epic: run `yad-epic`');
193
294
  log('');
194
295
  log(c.dim('Re-run anytime: `yad check` (report) / `yad check --fix` (reconcile).'));