yadflow 3.13.2 → 3.15.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 +14 -0
- package/bin/yad.mjs +3 -1
- package/cli/manifest.mjs +1 -1
- package/cli/next.mjs +60 -14
- package/cli/setup.mjs +5 -2
- package/package.json +1 -1
- package/skills/sdlc/config.yaml +1 -1
- package/skills/yad-connect-testing/SKILL.md +7 -3
- package/skills/yad-connect-testing/references/testing-context.md +6 -0
- package/skills/yad-connect-testing/references/testing-registry.md +2 -2
- package/skills/yad-test-cases/SKILL.md +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [3.15.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.14.0...v3.15.0) (2026-08-10)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **testing:** add maestro as a testing-tool adapter ([7718b50](https://github.com/abdelrahmannasr/yadflow/commit/7718b502814ee8e0eb46f5e6f20342fc2471dbd7))
|
|
7
|
+
|
|
8
|
+
# [3.14.0](https://github.com/abdelrahmannasr/yadflow/compare/v3.13.2...v3.14.0) (2026-08-10)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **next:** emit the action object with --json ([40d34dd](https://github.com/abdelrahmannasr/yadflow/commit/40d34ddba2492821700c7a877de28938faa74f3e))
|
|
14
|
+
|
|
1
15
|
## [3.13.2](https://github.com/abdelrahmannasr/yadflow/compare/v3.13.1...v3.13.2) (2026-08-10)
|
|
2
16
|
|
|
3
17
|
|
package/bin/yad.mjs
CHANGED
|
@@ -68,6 +68,8 @@ ${c.bold('Where am I / what next')}
|
|
|
68
68
|
yad next <epic> The single next action for one epic (skill or yad command)
|
|
69
69
|
yad next <epic> --check <step> Exit 0 if <step> is runnable now, else 1 (precondition guard)
|
|
70
70
|
yad next --all Every active epic's next action at once
|
|
71
|
+
yad next [<epic>] --json The same answer as a machine-readable action object (for
|
|
72
|
+
agents/CI) — always every epic, so --all is implied
|
|
71
73
|
yad skip <epic> ui-design --reason <text> Mark an optional step N/A for this epic (only
|
|
72
74
|
ui-design today) — a backend/API/data epic with no UI.
|
|
73
75
|
Stays visible & auditable (pre-done, gate short-circuited);
|
|
@@ -257,7 +259,7 @@ async function main() {
|
|
|
257
259
|
const [, epic] = o._;
|
|
258
260
|
// `--check` with no step is a malformed guard call — fail loudly rather than silently print.
|
|
259
261
|
if (o.check === true) { log(c.red('usage: yad next <epic> --check <step>')); process.exitCode = 1; break; }
|
|
260
|
-
await runNext(o.dir, { epic, check: typeof o.check === 'string' ? o.check : undefined, all: o.all });
|
|
262
|
+
await runNext(o.dir, { epic, check: typeof o.check === 'string' ? o.check : undefined, all: o.all, json: o.json });
|
|
261
263
|
break;
|
|
262
264
|
}
|
|
263
265
|
case 'skip': {
|
package/cli/manifest.mjs
CHANGED
|
@@ -135,7 +135,7 @@ export const DESIGN_PRIMARY = 'figma';
|
|
|
135
135
|
// is the fallback `registerTesting`/setup use when an unknown tool is named, and `none` is the explicit
|
|
136
136
|
// artifacts-only choice. (doctor does NOT fall back — an unknown tool there is a hard YAD-CFG-003 fail,
|
|
137
137
|
// mirroring the design-tool YAD-CFG-002.)
|
|
138
|
-
export const TESTING_TOOLS = ['playwright', 'cypress', 'pytest'];
|
|
138
|
+
export const TESTING_TOOLS = ['playwright', 'cypress', 'pytest', 'maestro'];
|
|
139
139
|
export const TESTING_PRIMARY = 'playwright';
|
|
140
140
|
|
|
141
141
|
// Supported learning-tool adapters (mirrors skills/sdlc/config.yaml `learning.tools`); `LEARNING_PRIMARY`
|
package/cli/next.mjs
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
// yad next <epic> the single next action for one epic
|
|
10
10
|
// yad next <epic> --check <step> exit 0 if <step> is runnable now, else 1 (the precondition guard)
|
|
11
11
|
// yad next --all every active epic's next action at once
|
|
12
|
+
// yad next [<epic>] --json the same answer as an action object, for an agent or CI
|
|
12
13
|
import fs from 'node:fs';
|
|
13
14
|
import path from 'node:path';
|
|
14
15
|
import { c, log, ok, info, warn, hand, fail, readJSON, exists } from './lib.mjs';
|
|
15
|
-
import { PROJECT_FILES } from './manifest.mjs';
|
|
16
|
+
import { PROJECT_FILES, VERSION } from './manifest.mjs';
|
|
16
17
|
import { epicRoot, loadLedger, nextAction, preconditionsMet, isValidEpicId, epicLineage, kindNoun, DISCOVERY_EPIC } from './epic-state.mjs';
|
|
17
18
|
|
|
18
19
|
// Is solo mode on? Persisted in hub.json by setup (Phase C/D); default false. Read defensively so a
|
|
@@ -37,6 +38,14 @@ function listEpics(root) {
|
|
|
37
38
|
.sort();
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// The action object for ONE epic, with its lineage kind attached. The single shape both surfaces
|
|
42
|
+
// consume — `printAction` renders it, `--json` emits it verbatim — so the prose and the machine
|
|
43
|
+
// answer can never drift apart.
|
|
44
|
+
const actionFor = (root, id) => ({
|
|
45
|
+
...nextAction(loadLedger(epicRoot(root, id)), { epic: id }),
|
|
46
|
+
lineageKind: epicLineage(root, id).kind,
|
|
47
|
+
});
|
|
48
|
+
|
|
40
49
|
// EP-istifta-inquiries-S03 → S03 (the compact lane label for the roll-up). Falls back to the full id.
|
|
41
50
|
const shortStory = (s) => (s && s.match(/S\d+$/i)?.[0]) || s || '(story)';
|
|
42
51
|
|
|
@@ -128,9 +137,7 @@ function generalNext(root, { all } = {}) {
|
|
|
128
137
|
const allEpics = listEpics(root);
|
|
129
138
|
const hasDiscovery = allEpics.includes(DISCOVERY_EPIC);
|
|
130
139
|
const featureEpics = allEpics.filter((id) => id !== DISCOVERY_EPIC);
|
|
131
|
-
const discoveryAction = hasDiscovery
|
|
132
|
-
? nextAction(loadLedger(epicRoot(root, DISCOVERY_EPIC)), { epic: DISCOVERY_EPIC })
|
|
133
|
-
: null;
|
|
140
|
+
const discoveryAction = hasDiscovery ? actionFor(root, DISCOVERY_EPIC) : null;
|
|
134
141
|
const discoveryOpen = !!discoveryAction && discoveryAction.kind !== 'discovery-done';
|
|
135
142
|
|
|
136
143
|
if (!featureEpics.length) {
|
|
@@ -142,10 +149,7 @@ function generalNext(root, { all } = {}) {
|
|
|
142
149
|
return;
|
|
143
150
|
}
|
|
144
151
|
|
|
145
|
-
const actions = featureEpics.map((id) => (
|
|
146
|
-
...nextAction(loadLedger(epicRoot(root, id)), { epic: id }),
|
|
147
|
-
lineageKind: epicLineage(root, id).kind,
|
|
148
|
-
}));
|
|
152
|
+
const actions = featureEpics.map((id) => actionFor(root, id));
|
|
149
153
|
if (discoveryOpen) printAction(discoveryAction, { solo }); // an unfinished discovery comes first
|
|
150
154
|
|
|
151
155
|
if (featureEpics.length === 1 || all) {
|
|
@@ -171,14 +175,59 @@ function checkPrecondition(root, epic, stepId) {
|
|
|
171
175
|
process.exitCode = 1;
|
|
172
176
|
}
|
|
173
177
|
|
|
178
|
+
// ---- machine-readable output (`--json`) --------------------------------------------------------
|
|
179
|
+
// `nextAction` already computes exactly what a caller needs; until now the ANSI prose renderer was
|
|
180
|
+
// its only consumer, so anything driving yadflow had to regex coloured English. This emits the SAME
|
|
181
|
+
// objects, unrendered. One envelope for every route, so a caller never has to branch on the shape:
|
|
182
|
+
//
|
|
183
|
+
// { version, ok: true, actions: [ <action>, … ] } next / next <epic>
|
|
184
|
+
// { version, ok, check: { epic, step, ok, reason } } next <epic> --check <step>
|
|
185
|
+
// { version, ok: true, setUp: false, actions: [] } the project has no `yad setup` yet
|
|
186
|
+
// { version, ok: false, error } bad epic id / no state.json
|
|
187
|
+
//
|
|
188
|
+
// Exit codes are unchanged from the prose path — only the rendering differs.
|
|
189
|
+
const emitJSON = (payload) => log(JSON.stringify({ version: VERSION, ...payload }, null, 2));
|
|
190
|
+
|
|
191
|
+
// A JSON error still leaves stdout parseable: a caller that pipes us into a parser gets an object
|
|
192
|
+
// explaining the failure, never half a document or a bare ANSI line.
|
|
193
|
+
function jsonError(message) {
|
|
194
|
+
emitJSON({ ok: false, error: message });
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// `--json` counterpart of runNext's three routes. Kept in one function so the routing reads next to
|
|
199
|
+
// the prose routing it mirrors.
|
|
200
|
+
function jsonNext(root, { epic, check }) {
|
|
201
|
+
if (epic && check) {
|
|
202
|
+
const res = preconditionsMet(loadLedger(epicRoot(root, epic)).state, check);
|
|
203
|
+
emitJSON({ ok: !!res.ok, check: { epic, step: check, ok: !!res.ok, ...(res.reason ? { reason: res.reason } : {}) } });
|
|
204
|
+
if (!res.ok) process.exitCode = 1;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (epic) {
|
|
208
|
+
if (!exists(path.join(epicRoot(root, epic), '.sdlc', 'state.json'))) {
|
|
209
|
+
return jsonError(`no epic state at epics/${epic}/.sdlc/state.json`);
|
|
210
|
+
}
|
|
211
|
+
return emitJSON({ ok: true, actions: [actionFor(root, epic)] });
|
|
212
|
+
}
|
|
213
|
+
if (!isSetUp(root)) return emitJSON({ ok: true, setUp: false, actions: [] });
|
|
214
|
+
// Every epic that HAS a ledger, discovery included — its `kind` already says whether it is open
|
|
215
|
+
// (`discovery-*`) or finished, so filtering it out would hide a fact rather than clarify one.
|
|
216
|
+
// `--all` is implied: an array always carries everything, so there is nothing left to expand.
|
|
217
|
+
return emitJSON({ ok: true, actions: listEpics(root).map((id) => actionFor(root, id)) });
|
|
218
|
+
}
|
|
219
|
+
|
|
174
220
|
// Entry point for the `next` command: route to the precondition check, a single epic's action, or the
|
|
175
221
|
// project-wide general view. Validates the epic id first.
|
|
176
|
-
export async function runNext(root, { epic, check, all } = {}) {
|
|
222
|
+
export async function runNext(root, { epic, check, all, json } = {}) {
|
|
177
223
|
if (epic && !isValidEpicId(epic)) {
|
|
178
|
-
|
|
224
|
+
const message = `invalid epic id: ${epic} (expected EP-<slug>, [a-z0-9-] only)`;
|
|
225
|
+
if (json) return jsonError(message);
|
|
226
|
+
fail(message);
|
|
179
227
|
process.exitCode = 1;
|
|
180
228
|
return;
|
|
181
229
|
}
|
|
230
|
+
if (json) return jsonNext(root, { epic, check });
|
|
182
231
|
if (epic && check) return checkPrecondition(root, epic, check);
|
|
183
232
|
if (!epic) return generalNext(root, { all });
|
|
184
233
|
|
|
@@ -189,8 +238,5 @@ export async function runNext(root, { epic, check, all } = {}) {
|
|
|
189
238
|
process.exitCode = 1;
|
|
190
239
|
return;
|
|
191
240
|
}
|
|
192
|
-
printAction(
|
|
193
|
-
{ ...nextAction(loadLedger(epicDir), { epic }), lineageKind: epicLineage(root, epic).kind },
|
|
194
|
-
{ solo: isSolo(root) },
|
|
195
|
-
);
|
|
241
|
+
printAction(actionFor(root, epic), { solo: isSolo(root) });
|
|
196
242
|
}
|
package/cli/setup.mjs
CHANGED
|
@@ -590,9 +590,12 @@ export async function runSetup(root, opts = {}) {
|
|
|
590
590
|
}
|
|
591
591
|
|
|
592
592
|
// Connect a testing tool (Playwright-first, pluggable; the test-cases step implements automation here)
|
|
593
|
-
|
|
593
|
+
// Banner + guide read TESTING_TOOLS rather than spelling the adapters out: this is the list that
|
|
594
|
+
// actually grows (maestro joined it), and a hardcoded copy here would quietly offer the user fewer
|
|
595
|
+
// tools than the prompt below accepts.
|
|
596
|
+
S(`Connect a testing tool (${TESTING_TOOLS.join(' / ')} / none)`);
|
|
594
597
|
guide([
|
|
595
|
-
|
|
598
|
+
`Where yad-test-cases generates automation. ${TESTING_TOOLS.join('/')}, or none for artifacts-only.`,
|
|
596
599
|
'Skipping is safe — test-cases authors test-cases.md only.',
|
|
597
600
|
]);
|
|
598
601
|
if (exists(testingPath) && !(await askYesNo('testing.json exists — reconfigure?', false))) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.0",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo, thread, reconcile). A BMAD module + 38 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
package/skills/sdlc/config.yaml
CHANGED
|
@@ -198,7 +198,7 @@ design:
|
|
|
198
198
|
# (test-links.json) is written by yad-test-cases per epic.
|
|
199
199
|
testing:
|
|
200
200
|
registry: "{project-root}/.sdlc/testing.json" # project-wide testing connection (NOT per-epic)
|
|
201
|
-
tools: [playwright, cypress, pytest]
|
|
201
|
+
tools: [playwright, cypress, pytest, maestro] # supported adapters; an unknown tool falls back to `primary`
|
|
202
202
|
primary: playwright # the default/named provider
|
|
203
203
|
degrade: artifacts-only # no tool / no MCP => yad-test-cases authors test-cases.md only
|
|
204
204
|
links: "{project-root}/epics/EP-<slug>/.sdlc/test-links.json" # per-epic case->test map (yad-test-cases)
|
|
@@ -21,7 +21,7 @@ connected, `yad-test-cases` runs artifacts-only exactly as before.
|
|
|
21
21
|
- `{project-root}` resolves from the project working directory (the **product hub**).
|
|
22
22
|
- The integration is **Playwright-first but pluggable** (`config.yaml` `testing.tools`): a testing-tool
|
|
23
23
|
*adapter*, like the GitHub/GitLab platform adapter or the design-tool adapter. Playwright is the
|
|
24
|
-
primary provider; `cypress` and `
|
|
24
|
+
primary provider; `cypress`, `pytest` and `maestro` are second providers; `none` → artifacts-only.
|
|
25
25
|
- **The testing tool is reached through its MCP** (a harness MCP server), NOT a subprocess CLI — the
|
|
26
26
|
same shape as the design tool's MCP, not Repomix's `npx`. The skill detects the MCP and degrades when
|
|
27
27
|
it is absent; it never installs an MCP server.
|
|
@@ -34,7 +34,7 @@ connected, `yad-test-cases` runs artifacts-only exactly as before.
|
|
|
34
34
|
## Inputs
|
|
35
35
|
|
|
36
36
|
- `action` — `connect` (default) | `refresh` | `list` | `disconnect`.
|
|
37
|
-
- `tool` — `playwright` | `cypress` | `pytest` | another adapter id (`config.yaml` `testing.tools`).
|
|
37
|
+
- `tool` — `playwright` | `cypress` | `pytest` | `maestro` | another adapter id (`config.yaml` `testing.tools`).
|
|
38
38
|
`none` records a deliberate artifacts-only project.
|
|
39
39
|
- `project_url` — the testing tool's project/config reference (e.g. a `playwright.config.ts` path or a
|
|
40
40
|
test-runner project URL). Optional — a connection with no suite yet is valid; `yad-test-cases` can
|
|
@@ -51,9 +51,13 @@ way `registerRepo` falls back on an unknown platform). Then **detect the tool's
|
|
|
51
51
|
- **playwright** → a Playwright MCP server (drives a browser, generates/runs E2E + API specs).
|
|
52
52
|
- **cypress** → the Cypress MCP (generate/run Cypress specs).
|
|
53
53
|
- **pytest** → a pytest MCP (generate/run service-layer tests).
|
|
54
|
+
- **maestro** → the Maestro MCP, bundled in the Maestro CLI and started as `maestro mcp` over STDIO
|
|
55
|
+
(drives iOS simulators, Android emulators and Chromium; generates/runs Maestro flows). Mobile E2E,
|
|
56
|
+
where Playwright has no reach.
|
|
54
57
|
- another adapter → its named MCP.
|
|
55
58
|
|
|
56
|
-
Record `provider` (the concrete MCP, e.g. `playwright-mcp` | `cypress-mcp` | `pytest-mcp`
|
|
59
|
+
Record `provider` (the concrete MCP, e.g. `playwright-mcp` | `cypress-mcp` | `pytest-mcp` |
|
|
60
|
+
`maestro-mcp`) and whether
|
|
57
61
|
it is available. **Auth is the local user's own** — the user's authenticated MCP session. The skill
|
|
58
62
|
**stores no tokens**; `project_url`/`suites` are plain references, never credentials.
|
|
59
63
|
|
|
@@ -15,6 +15,7 @@ Detection is best-effort against the user's own authenticated MCP session:
|
|
|
15
15
|
| `playwright` | a Playwright MCP | **generate** — author + run E2E/API specs against the app |
|
|
16
16
|
| `cypress` | the Cypress MCP | **generate** — author + run Cypress specs |
|
|
17
17
|
| `pytest` | a pytest MCP | **generate** — author + run service-layer tests |
|
|
18
|
+
| `maestro` | the Maestro MCP (`maestro mcp`, bundled in the CLI, STDIO) | **generate** — author + run Maestro flows on iOS/Android/Chromium |
|
|
18
19
|
| any | a read-only runner MCP | **link** — reference an existing suite and read results back |
|
|
19
20
|
| other | the adapter's named MCP | per that adapter |
|
|
20
21
|
|
|
@@ -34,6 +35,11 @@ the cases `test-cases.md` enumerates and the acceptance criteria the stories def
|
|
|
34
35
|
the code-maps from `yad-test-cases` Step 2b), one spec per high-priority (P0/P1) case, and runs them
|
|
35
36
|
via the MCP to confirm they execute.
|
|
36
37
|
- **Cypress / pytest** — the lens authors the equivalent specs in that framework's layout.
|
|
38
|
+
- **Maestro** — the lens authors `.yaml` flows under the repo's Maestro directory (one flow per
|
|
39
|
+
high-priority case), targeting the app id / screen elements the code-maps name rather than invented
|
|
40
|
+
ones, and runs them via the MCP against a simulator/emulator to confirm they execute. Mobile E2E is
|
|
41
|
+
the top of that pyramid: keep the flow count small and push detail down to the unit/integration
|
|
42
|
+
levels the repo already has.
|
|
37
43
|
|
|
38
44
|
Reuse what already exists: load the connected code repos' code-maps (`yad-test-cases` Step 2b) so
|
|
39
45
|
generated tests target real endpoints/components, not invented ones, and prefer the lowest useful test
|
|
@@ -14,8 +14,8 @@ root, not under any `epics/EP-<slug>/.sdlc/`.
|
|
|
14
14
|
|
|
15
15
|
```json
|
|
16
16
|
{
|
|
17
|
-
"tool": "playwright", // playwright | cypress | pytest | <adapter id> | none
|
|
18
|
-
"provider": "playwright-mcp", // the concrete MCP: playwright-mcp | cypress-mcp | pytest-mcp | null
|
|
17
|
+
"tool": "playwright", // playwright | cypress | pytest | maestro | <adapter id> | none
|
|
18
|
+
"provider": "playwright-mcp", // the concrete MCP: playwright-mcp | cypress-mcp | pytest-mcp | maestro-mcp | null
|
|
19
19
|
"project_url": "tests/playwright.config.ts", // project/config reference; null if none yet
|
|
20
20
|
"auth": "user", // ALWAYS the user's own MCP session — never a token
|
|
21
21
|
"suites": { "backend": null, "mobile": null }, // optional default suite refs per repo
|
|
@@ -94,7 +94,8 @@ Read `{project-root}/.sdlc/testing.json` (`config.yaml` `testing.registry`). Dec
|
|
|
94
94
|
the Markdown artifact only and record `testing: none` in the frontmatter with a one-line note
|
|
95
95
|
(mirrors the `design: none` degrade). Skip to Step 4.
|
|
96
96
|
- **A tool is connected and its MCP is available:** adopt the `test architect` lens and, using the
|
|
97
|
-
provider recorded in `testing.json` (Playwright via a Playwright MCP, Cypress/pytest via
|
|
97
|
+
provider recorded in `testing.json` (Playwright via a Playwright MCP, Cypress/pytest/Maestro via
|
|
98
|
+
theirs — Maestro authors mobile flows where Playwright has no reach) drive
|
|
98
99
|
`bmad-testarch-automate`:
|
|
99
100
|
- **Generate** — when the provider is write-capable, author one automation test per high-priority
|
|
100
101
|
(P0/P1) case into the connected code repo(s) for the repos in `epic.repos`, reusing the code-maps
|
|
@@ -120,7 +121,7 @@ status: draft
|
|
|
120
121
|
owner: <inherit from epic.md owner> # the epic owner carries through; not retyped
|
|
121
122
|
repos: [<inherit from epic>]
|
|
122
123
|
code-context: { repos: [], loaded: <YYYY-MM-DD or none> } # code-maps that informed the tests (Step 2b)
|
|
123
|
-
testing: <none | { tool: <playwright|cypress|pytest|…>, direction: <generated|linked>, suite: <url/path>, tests: <N> }> # the connected testing tool (Step 3b)
|
|
124
|
+
testing: <none | { tool: <playwright|cypress|pytest|maestro|…>, direction: <generated|linked>, suite: <url/path>, tests: <N> }> # the connected testing tool (Step 3b)
|
|
124
125
|
---
|
|
125
126
|
|
|
126
127
|
## Test strategy & risk
|