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 +2 -8
- package/README.md +47 -16
- package/cli/doctor.mjs +41 -1
- package/cli/epic-state.mjs +19 -2
- package/cli/errors.mjs +2 -0
- package/cli/manifest.mjs +20 -1
- package/cli/setup.mjs +111 -10
- package/docs/index.html +55 -15
- package/package.json +1 -1
- package/skills/sdlc/config.yaml +36 -4
- package/skills/sdlc/install.sh +1 -1
- package/skills/sdlc/module-help.csv +4 -1
- package/skills/yad-analysis/SKILL.md +10 -5
- package/skills/yad-connect-design/SKILL.md +117 -0
- package/skills/yad-connect-design/references/design-context.md +64 -0
- package/skills/yad-connect-design/references/design-registry.md +56 -0
- package/skills/yad-connect-testing/SKILL.md +121 -0
- package/skills/yad-connect-testing/references/testing-context.md +67 -0
- package/skills/yad-connect-testing/references/testing-registry.md +55 -0
- package/skills/yad-epic/SKILL.md +10 -5
- package/skills/yad-epic/references/state-schema.md +55 -11
- package/skills/yad-hub-bridge/SKILL.md +2 -2
- package/skills/yad-review-gate/SKILL.md +14 -11
- package/skills/yad-review-gate/references/gating.md +1 -1
- package/skills/yad-run/references/run-loop.md +3 -3
- package/skills/yad-spec/SKILL.md +3 -1
- package/skills/yad-status/SKILL.md +8 -5
- package/skills/yad-stories/SKILL.md +3 -1
- package/skills/yad-test-cases/SKILL.md +173 -0
- package/skills/yad-test-cases/references/test-cases-schema.md +70 -0
- package/skills/yad-ui/SKILL.md +64 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
# [2.
|
|
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
|
-
*
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
349
|
-
|
|
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
|
|
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
|
|
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;
|
package/cli/epic-state.mjs
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
135
|
-
step(4, total, 'Connect
|
|
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
|
-
//
|
|
159
|
-
step(
|
|
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
|
-
//
|
|
175
|
-
step(
|
|
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
|
-
//
|
|
186
|
-
step(
|
|
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).'));
|