yadflow 2.2.0 → 2.4.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 (34) hide show
  1. package/CHANGELOG.md +16 -5
  2. package/README.md +75 -22
  3. package/cli/doctor.mjs +45 -3
  4. package/cli/epic-state.mjs +19 -2
  5. package/cli/errors.mjs +2 -0
  6. package/cli/manifest.mjs +23 -1
  7. package/cli/setup.mjs +109 -10
  8. package/docs/index.html +62 -11
  9. package/package.json +5 -3
  10. package/skills/sdlc/config.yaml +41 -4
  11. package/skills/sdlc/install.sh +1 -1
  12. package/skills/sdlc/module-help.csv +6 -1
  13. package/skills/yad-analysis/SKILL.md +10 -5
  14. package/skills/yad-connect-design/SKILL.md +1 -1
  15. package/skills/yad-connect-design/references/design-registry.md +4 -2
  16. package/skills/yad-connect-learning/SKILL.md +140 -0
  17. package/skills/yad-connect-learning/references/learning-context.md +79 -0
  18. package/skills/yad-connect-learning/references/learning-registry.md +60 -0
  19. package/skills/yad-connect-testing/SKILL.md +121 -0
  20. package/skills/yad-connect-testing/references/testing-context.md +67 -0
  21. package/skills/yad-connect-testing/references/testing-registry.md +55 -0
  22. package/skills/yad-epic/SKILL.md +10 -5
  23. package/skills/yad-epic/references/state-schema.md +42 -11
  24. package/skills/yad-hub-bridge/SKILL.md +2 -2
  25. package/skills/yad-learn/SKILL.md +146 -0
  26. package/skills/yad-learn/references/learning-state.md +75 -0
  27. package/skills/yad-review-gate/SKILL.md +14 -11
  28. package/skills/yad-review-gate/references/gating.md +1 -1
  29. package/skills/yad-run/references/run-loop.md +3 -3
  30. package/skills/yad-spec/SKILL.md +3 -1
  31. package/skills/yad-status/SKILL.md +35 -15
  32. package/skills/yad-stories/SKILL.md +3 -1
  33. package/skills/yad-test-cases/SKILL.md +173 -0
  34. package/skills/yad-test-cases/references/test-cases-schema.md +70 -0
package/CHANGELOG.md CHANGED
@@ -1,16 +1,27 @@
1
- # [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-13)
1
+ # [2.4.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.3.0...v2.4.0) (2026-06-13)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * address code review on the design-tool connection ([1275146](https://github.com/abdelrahmannasr/yadflow/commit/1275146e9e24251d481d90eb26894a729e894997))
6
+ * address CodeRabbit review on PR [#48](https://github.com/abdelrahmannasr/yadflow/issues/48) ([2f182f7](https://github.com/abdelrahmannasr/yadflow/commit/2f182f72b68e226196b6190802771b0e12b585f9))
7
7
 
8
8
 
9
9
  ### Features
10
10
 
11
- * add yad-connect-design skill (Figma-first, pluggable) ([d1de46d](https://github.com/abdelrahmannasr/yadflow/commit/d1de46d2b08995db0228e60a58877649649b5cd0))
12
- * materialize the feature design in yad-ui (generate or link) ([e440571](https://github.com/abdelrahmannasr/yadflow/commit/e44057131e86fed45d9a9c4151cd762eb98f24e8))
13
- * wire the design-tool connection through the CLI ([1867ed4](https://github.com/abdelrahmannasr/yadflow/commit/1867ed433dd57025ae4143f5a236db32f1a99c41))
11
+ * add DeepTutor learning layer across all SDLC stages ([bd8d4ea](https://github.com/abdelrahmannasr/yadflow/commit/bd8d4eaaa0258242a62ed1b131f7e3f74506af64))
12
+ * make learning-layer output local-only (never committed or pushed) ([aa8f74e](https://github.com/abdelrahmannasr/yadflow/commit/aa8f74eb61855d3a663810a0c68cf8e37fbedd66))
13
+
14
+ # [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
15
+
16
+
17
+ ### Features
18
+
19
+ * 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))
20
+
21
+
22
+ ### Continuous Integration
23
+
24
+ * wire the hub's gate-sync + verified-commits CI and stamp the CLI version ([#46](https://github.com/abdelrahmannasr/yadflow/issues/46)) ([c856398](https://github.com/abdelrahmannasr/yadflow/commit/c856398a213b17aebea9c46204dbf955b92ea9cf))
14
25
 
15
26
  # [1.1.0](https://github.com/abdelrahmannasr/sdlc-workflow/compare/v1.0.3...v1.1.0) (2026-06-09)
16
27
 
package/README.md CHANGED
@@ -49,8 +49,11 @@ 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-connect-learning/` | Connect a learning tool (DeepTutor-first, pluggable) a CLI subprocess like Repomix; record `.sdlc/learning.json` + an optional grounded knowledge base. |
55
+ | `skills/yad-learn/` | The cross-cutting **learning layer**: tutor any member, at any stage, in the context of what's being built; records a personal, local-only skills log (gitignored, never committed/pushed). Opt-in, never gates. |
56
+ | `skills/yad-review-gate/` | The reusable **team review + approve gate** (used for all five reviews). |
54
57
  | `skills/yad-spec/` | Build Step A: run the Spec Kit ceremony once per story per repo → `specs/<story-id>/`. |
55
58
  | `skills/yad-implement/` | Build Step B: implement ONE atomic task as a small diff on its own branch. |
56
59
  | `skills/yad-checks/` | Build Step C: wire + run the CI gates (spec-link, contract-check, build/test/lint, verified-commits). |
@@ -122,18 +125,24 @@ a manual `yad gate sync` racing CI, or GitLab pipelines — two simultaneous syn
122
125
  *commits* via the rebase retry but each works from the state it read at start, so the rarer of two
123
126
  simultaneous advancements can be lost; the next event or scheduled sweep re-syncs and converges.
124
127
 
125
- ### What `setup` walks you through (8 steps)
128
+ ### What `setup` walks you through (10 steps)
126
129
 
127
130
  1. **Preflight** — confirm the hub is a git repo (offers `git init`); check `git`/`node`/`npx`.
128
- 2. **Install the module** — copy all 18 `yad-*` skills into the IDE skill dirs you pick
131
+ 2. **Install the module** — copy all 22 `yad-*` skills into the IDE skill dirs you pick
129
132
  (`.claude/`, `.agents/`, `.zencoder/`, `.opencode/`) and register `_bmad/sdlc/`.
130
133
  3. **Hub platform & roster** — detect GitHub/GitLab from the remote; record reviewers → `.sdlc/hub.json`.
131
134
  4. **Connect a design tool** — record the design tool (Figma / pencil / none) → `.sdlc/design.json` so
132
135
  the UI step can materialize the design; the MCP itself is confirmed later by `yad-connect-design`.
133
- 5. **Connect code repos** — register each repo into `.sdlc/repos.json` and cache a Repomix pack.
134
- 6. **Wire each repo** CI gates, PR/MR template, and review-comment scaffold.
135
- 7. **AI review** — optionally write `.coderabbit.yaml`.
136
- 8. **Done** stamp `.sdlc/cli-version.json` and hand off the AI-only steps (code-maps; first epic).
136
+ 5. **Connect a testing tool** — record the testing tool (Playwright / cypress / pytest / none) →
137
+ `.sdlc/testing.json` so the test-cases step can implement the automation; the MCP itself is confirmed
138
+ later by `yad-connect-testing`.
139
+ 6. **Connect a learning tool** record the learning tool (DeepTutor / none) → `.sdlc/learning.json` so
140
+ the learning layer can tutor the team; the CLI + knowledge base are confirmed later by
141
+ `yad-connect-learning`.
142
+ 7. **Connect code repos** — register each repo into `.sdlc/repos.json` and cache a Repomix pack.
143
+ 8. **Wire each repo** — CI gates, PR/MR template, and review-comment scaffold.
144
+ 9. **AI review** — optionally write `.coderabbit.yaml`.
145
+ 10. **Done** — stamp `.sdlc/cli-version.json` and hand off the AI-only steps (code-maps; first epic).
137
146
 
138
147
  The deterministic file work runs automatically; the AI-only steps are handed to the Claude Code skills
139
148
  with a printed next-action. Re-run `… check --fix` any time the workflow updates — it never re-asks for
@@ -170,10 +179,12 @@ with a fix-it hint per finding. Failures carry stable, greppable codes, also pri
170
179
  | `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 |
171
180
  | `YAD-CFG-001` | `hub.json` names an unknown platform | expected `github`, `gitlab`, or `null` — fix it or re-run `yad setup` |
172
181
  | `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` |
182
+ | `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` |
183
+ | `YAD-CFG-004` | `learning.json` names an unknown learning tool | expected one of `config.yaml` `learning.tools` (e.g. `deeptutor`), or `none` — fix it or re-run `yad setup` |
173
184
 
174
185
  Filing a bug? Attach `yad doctor --json` — it contains no secrets (names, paths, and check results only).
175
186
 
176
- ## Agent skills (all 18)
187
+ ## Agent skills (all 22)
177
188
 
178
189
  The CLI **installs and wires** the module; the skills below are the **agents you invoke by name** in your
179
190
  AI IDE (e.g. *“run `yad-epic`”*) to actually do the work. State lives in files you can also edit
@@ -191,12 +202,36 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
191
202
  Records the tool + project/file references in `.sdlc/design.json` (local-user / MCP-session auth, no
192
203
  stored tokens), detecting the design-tool MCP and degrading to markdown-only when absent. Idempotent
193
204
  and refreshable; one connection per project.
205
+ - **`yad-connect-testing`** — Connects a testing tool (Playwright-first, pluggable) so the test-cases
206
+ step can implement the actual automation tests inside it, alongside the Markdown. Records the tool +
207
+ project/suite references in `.sdlc/testing.json` (local-user / MCP-session auth, no stored tokens),
208
+ detecting the testing-tool MCP and degrading to artifacts-only when absent. Idempotent and
209
+ refreshable; one connection per project.
210
+ - **`yad-connect-learning`** — Connects a learning/tutoring tool (DeepTutor-first, pluggable) so the
211
+ cross-cutting learning layer can tutor any team member in the context of what's being built. Records
212
+ the tool + an optional grounded knowledge base in `.sdlc/learning.json` (local-user auth, no stored
213
+ tokens), detecting the **DeepTutor CLI on PATH** (a subprocess like Repomix — DeepTutor ships no MCP)
214
+ and degrading to **harness-native** tutoring when absent. Idempotent and refreshable; one connection
215
+ per project.
216
+
217
+ ### The learning layer (cross-cutting — any member, any stage)
218
+
219
+ - **`yad-learn`** — At **any** SDLC stage, a team member can ask to learn a concept and be tutored *in
220
+ the context of what the team is building* — e.g. *"teach me why the architecture hash-locks the
221
+ contract surface"*. Routes the request to the connected learning tool (`.sdlc/learning.json`,
222
+ DeepTutor-first) grounded in the project knowledge base, or degrades to **harness-native** tutoring
223
+ (the harness model reading the artifacts) when nothing is connected — so it always works. Renders a
224
+ tutorial artifact and appends to a per-member **learning ledger** kept **local-only** (gitignored,
225
+ never committed or pushed — to the hub or any code repo) so it stays a private, personal **skills log**
226
+ (`yad-status` rolls up the local records). **Purely opt-in — it never blocks a gate** and
227
+ never touches epic state, approvals, or the contract lock. *AI builds, the hand decides* — and now the
228
+ hand can also learn, on demand, what it is deciding about.
194
229
 
195
230
  ### Front half — author the "thinking" (once per epic, human-gated)
196
231
 
197
232
  - **`yad-analysis`** — *Optional* front state 1. With the analyst, pressure-test a feature idea
198
233
  and write the discovery brief into `analysis.md`. Assigns the `EP-<slug>` ID and seeds `.sdlc/` state
199
- (the 10-step chain that puts analysis before epic). If skipped, the epic step does this shaping inline.
234
+ (the 12-step chain that puts analysis before epic). If skipped, the epic step does this shaping inline.
200
235
  - **`yad-epic`** — The epic front state. Shape the idea with the analyst (or read `analysis.md`
201
236
  when it already ran), then write the epic with the pm into `epic.md`. The entry point when analysis is
202
237
  skipped: assigns the `EP-<slug>` ID and seeds `.sdlc/` state.
@@ -211,6 +246,13 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
211
246
  - **`yad-stories`** — Front state 7. With the pm, break the approved epic into user stories, each
212
247
  tagged with the repos that must implement it. Assigns zero-padded `EP-<slug>-S0N` IDs, one file per
213
248
  story under `stories/`. Reads epic + architecture + contract + UI.
249
+ - **`yad-test-cases`** — Front state 9, a **parallel, non-blocking** track: it opens when the stories
250
+ gate passes (the epic is already `ready-for-build`, so the build half runs alongside it). With the
251
+ test architect (Murat), author `test-cases.md` covering the approved stories (risk-based P0–P3 cases +
252
+ story→case traceability). When a testing tool is connected (`yad-connect-testing`), also **implements
253
+ the automation tests** in the connected code repo(s) (generate or link), recording the case→test map in
254
+ `test-links.json`; degrades to artifacts-only otherwise. Reads epic + architecture + contract + UI +
255
+ stories.
214
256
 
215
257
  ### The review gate (cross-cutting — used by every review)
216
258
 
@@ -269,7 +311,7 @@ merging the approved, fully-resolved review PR — never on a machine.
269
311
 
270
312
  As of **Phase 4a** the `automation` dial is no longer inert: the orchestrator `yad-run` reads it and,
271
313
  for the safe **back** steps, advances on its own when a step is set to `machine_advance` (and has
272
- *earned* it — see "Run the back half on the dial" below). The engineer review and all four front
314
+ *earned* it — see "Run the back half on the dial" below). The engineer review and all five front
273
315
  states stay `human_approve` forever.
274
316
 
275
317
  ## Using the workflow end to end (all the steps, in order)
@@ -280,17 +322,19 @@ detailed sections below expand every phase. Invoke a skill by name in your agent
280
322
 
281
323
  ### 0 — One-time setup
282
324
 
283
- > **Shortcut:** `npx yadflow setup` walks through steps 1, 4, 5 and 6 below
284
- > interactively (module install, hub detect + roster, connect repos, wire each repo). Run
285
- > `… check --fix` any time afterwards to reconcile. The manual steps below are the long-hand
286
- > equivalent and still work.
325
+ > **Shortcut:** `npx yadflow setup` runs the guided wizard interactively module install, hub
326
+ > detect + roster, connect a design/testing/learning tool (each optional), connect repos, wire each
327
+ > repo. Run `… check --fix` any time afterwards to reconcile. The manual steps below are the
328
+ > long-hand equivalent and still work.
287
329
 
288
330
  1. **Install the module:** `bash skills/sdlc/install.sh` (re-run after any BMAD update).
289
331
  2. **Have your code repo(s).** They are **separate git repos** (one `.git` each). For the demo they
290
332
  live under `demo-repos/<repo>/` — regenerate from `demo-repos/README.md`.
291
333
  3. **Optional tools** (the workflow degrades gracefully and records it if any are absent): **Spec Kit**
292
334
  (`/speckit.*`), **Impeccable** (`/impeccable …`), **Repomix** (`npx repomix`, used by
293
- `yad-connect-repos` and `yad-backfill`), **CodeRabbit** (advisory AI review).
335
+ `yad-connect-repos` and `yad-backfill`), **CodeRabbit** (advisory AI review), **DeepTutor**
336
+ (`deeptutor`, the learning layer's tutor — degrades to harness-native, used by `yad-connect-learning`
337
+ and `yad-learn`).
294
338
  4. **Wire each code repo once:** `yad-checks repo:<repo> action: wire` (installs the CI gates —
295
339
  *merges* with any existing CI, never clobbers), `yad-pr-template repo:<repo> action: wire` (PR/MR
296
340
  template + risk routing), `yad-review-comments repo:<repo> action: wire` (review-comment scaffold).
@@ -301,11 +345,16 @@ detailed sections below expand every phase. Invoke a skill by name in your agent
301
345
  (SSH or credential helper; GitHub or GitLab; no stored tokens). Re-run for any new repo. Freshness is a
302
346
  **human decision**: `yad repo list` shows fresh/stale, `yad repo refresh [name]` re-packs a moved repo
303
347
  (skills flag staleness and point here — they never silently re-pack). Greenfield → skip it.
304
- 6. **(Optional) Put the hub on a platform** so the front-half review runs through real PRs:
348
+ 6. **(Optional) Connect tools** so the matching steps do real work (each degrades gracefully and is
349
+ recorded if absent): `yad-connect-design action: connect` (Figma-first → `design.json`, lets
350
+ `yad-ui` materialize screens), `yad-connect-testing action: connect` (Playwright-first →
351
+ `testing.json`, lets `yad-test-cases` implement automation), `yad-connect-learning action: connect`
352
+ (DeepTutor-first → `learning.json`, powers the cross-cutting learning layer).
353
+ 7. **(Optional) Put the hub on a platform** so the front-half review runs through real PRs:
305
354
  `yad-connect-repos action: detect-hub`, then `action: roster` once per reviewer (login → SDLC
306
355
  name + role), and `yad-pr-template repo:hub action: wire` / `yad-review-comments repo:hub action:
307
356
  wire` / `yad-checks repo:hub action: wire`. With no hub platform the front gate just runs file-only.
308
- 7. **Conventions:** commits and PR/MR titles follow Conventional Commits (lowercase after the type), the
357
+ 8. **Conventions:** commits and PR/MR titles follow Conventional Commits (lowercase after the type), the
309
358
  human author owns each commit with an optional per-commit `Co-Authored-By` AI trailer — see
310
359
  [`CONTRIBUTING.md`](CONTRIBUTING.md).
311
360
 
@@ -320,7 +369,10 @@ threads are resolved. Details: **“Run the full front half by hand”** below.
320
369
  7. `yad-architecture` → `architecture.md` + locked `contract.md` → review (**escalated**: contract).
321
370
  8. `yad-ui` → `ui-design.md` + `DESIGN.md` → review (base rule).
322
371
  9. `yad-stories` → repo-tagged `stories/EP-<slug>-S0N.md` → review (**per-repo**).
323
- → `state.json` reaches `currentStep: ready-for-build`.
372
+ → `state.json` reaches `currentStep: ready-for-build` — **the build half can start now.**
373
+ 10. `yad-test-cases` → `test-cases.md` (+ automation tests when a testing tool is connected) → review (base rule).
374
+ **Parallel, non-blocking:** opens when the stories gate passes and runs alongside the build half; its
375
+ review never moves `currentStep` off `ready-for-build`.
324
376
 
325
377
  ### B — Build half (per story, per repo)
326
378
  From a `ready-for-build` story, for **each** repo the story is tagged with. Details: **“Run the full
@@ -355,12 +407,13 @@ Details: **“Run the back half on the dial”** below.
355
407
  ## Run the full front half by hand
356
408
 
357
409
  The front half walks **epic → review → architecture+contract → review → UI design → review → stories
358
- → review → `ready-for-build`**. It is all files under `epics/EP-<slug>/`. The skills below guide you,
359
- but you can also edit the files directly that's the point.
410
+ → review → `ready-for-build`**, then **test cases review** runs as a **parallel, non-blocking track**
411
+ alongside the build half. It is all files under `epics/EP-<slug>/`. The skills below guide you, but you
412
+ can also edit the files directly — that's the point.
360
413
 
361
414
  Each authoring step is the same shape: an author skill produces an artifact, sets its step `done`,
362
415
  moves `currentStep` to the matching review, and **stops at the gate**. Then **`yad-review-gate`**
363
- (one gate, reused for all four reviews) takes `open → comment → approve → advance`. When the hub is on a
416
+ (one gate, reused for all five reviews) takes `open → comment → approve → advance`. When the hub is on a
364
417
  platform, the **`yad gate`** CLI runs that gate over a real PR/MR — `open` raises the review PR, `sync`
365
418
  pulls approvals + comment threads into the ledger, and the step **auto-advances when the approved,
366
419
  fully-resolved PR is merged** (the merge is the human approval act).
@@ -417,7 +470,7 @@ accumulate, and the step moves forward only when the rule is met. **File-only**
417
470
  in any story's `repos`**.
418
471
 
419
472
  ### Check status anytime
420
- Invoke **`yad-status`** (read-only) to see the full 8-step chain, every step's dials/status, the
473
+ Invoke **`yad-status`** (read-only) to see the full 10-step chain, every step's dials/status, the
421
474
  contract lock, story repo tags, and which approvals the active gate still needs.
422
475
 
423
476
  ## 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, DESIGN_TOOLS } from './manifest.mjs';
8
+ import { VERSION, PROJECT_FILES, DESIGN_TOOLS, TESTING_TOOLS, LEARNING_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';
@@ -88,13 +88,55 @@ export function projectChecks(checks, root) {
88
88
  }
89
89
  if (designBroken) { /* reported above */ }
90
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_TOOLS, 'none', null, undefined].includes(design.tool)) check(checks, 'design', 'project', 'fail', `${PROJECT_FILES.designConfig}: unknown design tool '${design.tool}' [YAD-CFG-002]`, `expected one of ${DESIGN_TOOLS.join(', ')}, or none`);
92
- else if (!design.tool || design.tool === 'none') check(checks, 'design', 'project', 'ok', 'design: markdown-only');
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
93
  else if (design.source && design.source !== 'unavailable') check(checks, 'design', 'project', 'ok', `design: ${design.tool} (${design.source})`);
94
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
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
96
  }
97
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
+
118
+ // learning.json: parse + shape + tool + CLI confirmation (absent is the normal harness-native default —
119
+ // pre-feature projects have none, so silence rather than warn when the file does not exist). DeepTutor
120
+ // has no MCP, so `source` is deeptutor-cli (found on PATH) or harness-native (degraded).
121
+ const learningPath = path.join(root, PROJECT_FILES.learningConfig);
122
+ if (exists(learningPath)) {
123
+ let learning = null, learningBroken = false;
124
+ try {
125
+ learning = readJSONStrict(learningPath, null);
126
+ } catch (e) {
127
+ learningBroken = true;
128
+ check(checks, 'learning', 'project', 'fail', `${PROJECT_FILES.learningConfig} does not parse [${e.code || 'YAD-STATE-001'}]`, e.hint || 'fix the JSON or restore it from git');
129
+ }
130
+ if (learningBroken) { /* reported above */ }
131
+ else if (typeof learning !== 'object' || Array.isArray(learning) || learning === null) check(checks, 'learning', 'project', 'fail', `${PROJECT_FILES.learningConfig} has the wrong shape [YAD-STATE-002]`, 'expected a JSON object');
132
+ else if (learning.tool === 'none') check(checks, 'learning', 'project', 'ok', 'learning: harness-native');
133
+ else if (!LEARNING_TOOLS.includes(learning.tool)) check(checks, 'learning', 'project', 'fail', `${PROJECT_FILES.learningConfig}: unknown or missing learning tool '${learning.tool}' [YAD-CFG-004]`, `expected one of ${LEARNING_TOOLS.join(', ')}, or none`);
134
+ else if (learning.source === 'deeptutor-cli') check(checks, 'learning', 'project', 'ok', `learning: ${learning.tool} (${learning.source})`);
135
+ else if (learning.source === 'harness-native') check(checks, 'learning', 'project', 'warn', `learning: ${learning.tool} CLI unavailable — yad-learn tutors harness-native`, 'install the deeptutor CLI, then run `yad-connect-learning` (action: refresh)');
136
+ else if (learning.source == null) check(checks, 'learning', 'project', 'warn', `learning: ${learning.tool} recorded but the CLI is not confirmed`, 'run `yad-connect-learning` in Claude Code to detect the CLI');
137
+ else check(checks, 'learning', 'project', 'fail', `${PROJECT_FILES.learningConfig}: unknown source '${learning.source}' [YAD-STATE-002]`, 'expected deeptutor-cli, harness-native, or null');
138
+ }
139
+
98
140
  // repos.json: parse + every entry is a live git repo; staleness vs syncedHead
99
141
  let registry = { repos: [] };
100
142
  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
@@ -21,6 +21,8 @@ export const CODES = {
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
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)',
25
+ 'YAD-CFG-004': 'learning.json names an unknown learning tool (expected one of config.yaml learning.tools, or none)',
24
26
  };
25
27
 
26
28
  export const err = (code, message, hint) => new YadError(code, message, hint);
package/cli/manifest.mjs CHANGED
@@ -10,15 +10,19 @@ 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 18 hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
13
+ // The 22 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',
21
22
  'yad-connect-design',
23
+ 'yad-connect-testing',
24
+ 'yad-connect-learning',
25
+ 'yad-learn',
22
26
  'yad-spec',
23
27
  'yad-implement',
24
28
  'yad-checks',
@@ -87,11 +91,29 @@ export const MODULE_FILES = ['config.yaml', 'module-help.csv'];
87
91
  export const DESIGN_TOOLS = ['figma', 'pencil'];
88
92
  export const DESIGN_PRIMARY = 'figma';
89
93
 
94
+ // Supported testing-tool adapters (mirrors skills/sdlc/config.yaml `testing.tools`); `TESTING_PRIMARY`
95
+ // is the fallback `registerTesting`/setup use when an unknown tool is named, and `none` is the explicit
96
+ // artifacts-only choice. (doctor does NOT fall back — an unknown tool there is a hard YAD-CFG-003 fail,
97
+ // mirroring the design-tool YAD-CFG-002.)
98
+ export const TESTING_TOOLS = ['playwright', 'cypress', 'pytest'];
99
+ export const TESTING_PRIMARY = 'playwright';
100
+
101
+ // Supported learning-tool adapters (mirrors skills/sdlc/config.yaml `learning.tools`); `LEARNING_PRIMARY`
102
+ // is the fallback `registerLearning`/setup use when an unknown tool is named, and `none` is the explicit
103
+ // harness-native choice (yad-learn tutors via the harness model when no tool is connected). DeepTutor is
104
+ // a CLI subprocess (no MCP), so the connect skill detects the binary — not an MCP — but the registry +
105
+ // degrade shape mirrors design/testing. (doctor does NOT fall back — an unknown tool there is a hard
106
+ // YAD-CFG-004 fail, mirroring the design-tool YAD-CFG-002.)
107
+ export const LEARNING_TOOLS = ['deeptutor'];
108
+ export const LEARNING_PRIMARY = 'deeptutor';
109
+
90
110
  // Project-level files setup produces (used by `check` to spot missing setup).
91
111
  export const PROJECT_FILES = {
92
112
  reposRegistry: '.sdlc/repos.json',
93
113
  hubConfig: '.sdlc/hub.json',
94
114
  designConfig: '.sdlc/design.json',
115
+ testingConfig: '.sdlc/testing.json',
116
+ learningConfig: '.sdlc/learning.json',
95
117
  version: '.sdlc/cli-version.json',
96
118
  };
97
119
 
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, DESIGN_TOOLS, DESIGN_PRIMARY } from './manifest.mjs';
8
+ import { VERSION, IDE_FOLDER_TARGETS, PROJECT_FILES, DESIGN_TOOLS, DESIGN_PRIMARY, TESTING_TOOLS, TESTING_PRIMARY, LEARNING_TOOLS, LEARNING_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'];
@@ -87,6 +87,62 @@ export function registerDesign(root, { tool, project_url = null, files = null, t
87
87
  return design;
88
88
  }
89
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
+
118
+ // Record the project's learning-tool connection into .sdlc/learning.json (the deterministic half of the
119
+ // connect loop; CLI detection + the kb build are AI steps, handed off to `yad-connect-learning`). An
120
+ // unknown tool falls back to the primary adapter rather than being rejected — mirrors registerDesign/
121
+ // registerTesting. `none` is the explicit harness-native choice (yad-learn tutors via the harness model).
122
+ // DeepTutor has no MCP, so `source` stays null at setup until the connect skill detects the CLI on PATH.
123
+ export function registerLearning(root, { tool, kb = null, today = null } = {}) {
124
+ // Idempotent re-connect: carry the original first-connect date forward; only lastSyncedAt moves.
125
+ const learningPath = path.join(root, PROJECT_FILES.learningConfig);
126
+ const prev = readJSON(learningPath, null);
127
+ const connectedAt = prev && prev.connectedAt ? prev.connectedAt : today;
128
+ let t = (tool || '').toLowerCase();
129
+ if (t === 'none' || t === '') {
130
+ const off = { tool: 'none', provider: null, version: null, kb: null, kb_sources: [], auth: 'user',
131
+ connectedAt, lastSyncedAt: today, source: 'harness-native' };
132
+ writeJSON(learningPath, off);
133
+ return off;
134
+ }
135
+ if (!LEARNING_TOOLS.includes(t)) { warn(`unknown learning tool '${tool}' — using ${LEARNING_PRIMARY}`); t = LEARNING_PRIMARY; }
136
+ // source stays null until `yad-connect-learning` detects the CLI on PATH (AI step). doctor reports a
137
+ // recorded-but-unconfirmed connection as a warning pointing at that skill.
138
+ const learning = {
139
+ tool: t, provider: null, version: null, kb: kb || null, kb_sources: [], auth: 'user',
140
+ connectedAt, lastSyncedAt: today, source: null,
141
+ };
142
+ writeJSON(learningPath, learning);
143
+ return learning;
144
+ }
145
+
90
146
  function applyActions(actions, { force = false } = {}) {
91
147
  let changed = 0;
92
148
  for (const a of actions) {
@@ -100,7 +156,7 @@ function applyActions(actions, { force = false } = {}) {
100
156
  }
101
157
 
102
158
  export async function runSetup(root, opts = {}) {
103
- const total = 8;
159
+ const total = 10;
104
160
  log(c.bold(`\nSDLC Workflow setup ${c.dim('v' + VERSION)}`));
105
161
  log(c.dim(`target: ${root}`));
106
162
 
@@ -178,8 +234,43 @@ export async function runSetup(root, opts = {}) {
178
234
  : `wrote ${PROJECT_FILES.designConfig} (${tool})`);
179
235
  }
180
236
 
181
- // 5. Connect code repos
182
- step(5, total, 'Connect code repos');
237
+ // 5. Connect a testing tool (Playwright-first, pluggable; the test-cases step implements automation here)
238
+ step(5, total, 'Connect a testing tool (playwright / cypress / pytest / none)');
239
+ const testingPath = path.join(root, PROJECT_FILES.testingConfig);
240
+ if (exists(testingPath) && !(await askYesNo('testing.json exists — reconfigure?', false))) {
241
+ info('keeping existing .sdlc/testing.json');
242
+ } else {
243
+ let tool = (await ask(`Testing tool (${TESTING_TOOLS.join('/')}/none)`, TESTING_PRIMARY)).toLowerCase();
244
+ if (![...TESTING_TOOLS, 'none'].includes(tool)) {
245
+ warn(`unknown testing tool '${tool}' — using ${TESTING_PRIMARY}`);
246
+ tool = TESTING_PRIMARY;
247
+ }
248
+ const project_url = tool === 'none' ? null : (await ask(' project/config reference (blank to set later)', '')) || null;
249
+ registerTesting(root, { tool, project_url, today: opts.today ?? null });
250
+ ok(tool === 'none'
251
+ ? `wrote ${PROJECT_FILES.testingConfig} (artifacts-only)`
252
+ : `wrote ${PROJECT_FILES.testingConfig} (${tool})`);
253
+ }
254
+
255
+ // 6. Connect a learning tool (DeepTutor-first, pluggable; the learning layer tutors team members here)
256
+ step(6, total, 'Connect a learning tool (deeptutor / none)');
257
+ const learningPath = path.join(root, PROJECT_FILES.learningConfig);
258
+ if (exists(learningPath) && !(await askYesNo('learning.json exists — reconfigure?', false))) {
259
+ info('keeping existing .sdlc/learning.json');
260
+ } else {
261
+ let tool = (await ask(`Learning tool (${LEARNING_TOOLS.join('/')}/none)`, LEARNING_PRIMARY)).toLowerCase();
262
+ if (![...LEARNING_TOOLS, 'none'].includes(tool)) {
263
+ warn(`unknown learning tool '${tool}' — using ${LEARNING_PRIMARY}`);
264
+ tool = LEARNING_PRIMARY;
265
+ }
266
+ registerLearning(root, { tool, today: opts.today ?? null });
267
+ ok(tool === 'none'
268
+ ? `wrote ${PROJECT_FILES.learningConfig} (harness-native)`
269
+ : `wrote ${PROJECT_FILES.learningConfig} (${tool})`);
270
+ }
271
+
272
+ // 7. Connect code repos
273
+ step(7, total, 'Connect code repos');
183
274
  const regPath = path.join(root, PROJECT_FILES.reposRegistry);
184
275
  const registry = readJSON(regPath, { repos: [] });
185
276
  const known = new Set(registry.repos.map((r) => r.name));
@@ -202,8 +293,8 @@ export async function runSetup(root, opts = {}) {
202
293
  }
203
294
  }
204
295
 
205
- // 6. Wire each connected repo + the hub itself
206
- step(6, total, 'Wire connected repos + the hub (CI gates, PR template, comment scaffold, gate-sync)');
296
+ // 8. Wire each connected repo + the hub itself
297
+ step(8, total, 'Wire connected repos + the hub (CI gates, PR template, comment scaffold, gate-sync)');
207
298
  if (registry.repos.length === 0) info('no repos to wire');
208
299
  for (const repo of registry.repos) {
209
300
  log(` ${c.bold(repo.name)} ${c.dim(`(${repo.platform})`)}`);
@@ -218,8 +309,8 @@ export async function runSetup(root, opts = {}) {
218
309
  // author allowlists for the verified-commits gate (hub + every repo), from the roster emails
219
310
  applyActions(authorsActions(root, registry.repos), { force: true });
220
311
 
221
- // 7. Optional CodeRabbit
222
- step(7, total, 'AI review (CodeRabbit)');
312
+ // 9. Optional CodeRabbit
313
+ step(9, total, 'AI review (CodeRabbit)');
223
314
  for (const repo of registry.repos) {
224
315
  const cr = path.join(path.resolve(root, repo.path), '.coderabbit.yaml');
225
316
  if (exists(cr)) { info(`${repo.name}: .coderabbit.yaml present`); continue; }
@@ -229,8 +320,8 @@ export async function runSetup(root, opts = {}) {
229
320
  }
230
321
  }
231
322
 
232
- // 8. Summary + version stamp
233
- step(8, total, 'Done');
323
+ // 10. Summary + version stamp
324
+ step(10, total, 'Done');
234
325
  writeJSON(path.join(root, PROJECT_FILES.version), { version: VERSION, ideTargets, updatedAt: opts.today ?? null });
235
326
  ok(`stamped ${PROJECT_FILES.version} (v${VERSION})`);
236
327
  log('');
@@ -240,6 +331,14 @@ export async function runSetup(root, opts = {}) {
240
331
  if (design && design.tool && design.tool !== 'none') {
241
332
  hand(`confirm the design tool: run \`yad-connect-design\` to detect the ${design.tool} MCP (or it degrades to markdown-only)`);
242
333
  }
334
+ const testing = readJSON(testingPath, null);
335
+ if (testing && testing.tool && testing.tool !== 'none') {
336
+ hand(`confirm the testing tool: run \`yad-connect-testing\` to detect the ${testing.tool} MCP (or it degrades to artifacts-only)`);
337
+ }
338
+ const learning = readJSON(learningPath, null);
339
+ if (learning && learning.tool && learning.tool !== 'none') {
340
+ hand(`confirm the learning tool: run \`yad-connect-learning\` to detect the ${learning.tool} CLI (or it degrades to harness-native)`);
341
+ }
243
342
  hand('author your first epic: run `yad-epic`');
244
343
  log('');
245
344
  log(c.dim('Re-run anytime: `yad check` (report) / `yad check --fix` (reconcile).'));