yadflow 2.0.1 β†’ 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,16 @@
1
- ## [2.0.1](https://github.com/abdelrahmannasr/yadflow/compare/v2.0.0...v2.0.1) (2026-06-12)
1
+ # [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-13)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * publish README with pre-rendered SVG diagrams so they display on npm ([05382f6](https://github.com/abdelrahmannasr/yadflow/commit/05382f6bfe27bb0604165692ca6fe1cdb74b9a35))
6
+ * address code review on the design-tool connection ([1275146](https://github.com/abdelrahmannasr/yadflow/commit/1275146e9e24251d481d90eb26894a729e894997))
7
+
8
+
9
+ ### Features
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))
7
14
 
8
15
  # [1.1.0](https://github.com/abdelrahmannasr/sdlc-workflow/compare/v1.0.3...v1.1.0) (2026-06-09)
9
16
 
package/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  [![npm version](https://img.shields.io/npm/v/yadflow?logo=npm)](https://www.npmjs.com/package/yadflow)
4
4
  [![CI](https://github.com/abdelrahmannasr/yadflow/actions/workflows/ci.yml/badge.svg)](https://github.com/abdelrahmannasr/yadflow/actions/workflows/ci.yml)
5
5
  [![provenance](https://img.shields.io/badge/npm-provenance-blue?logo=npm)](https://docs.npmjs.com/generating-provenance-statements)
6
+ [![node](https://img.shields.io/node/v/yadflow?logo=node.js)](https://github.com/abdelrahmannasr/yadflow/blob/main/package.json)
7
+ [![security policy](https://img.shields.io/badge/security-policy-brightgreen)](https://github.com/abdelrahmannasr/yadflow/blob/main/SECURITY.md)
6
8
  [![report](https://img.shields.io/badge/docs-Yadflow%20report-2471a3)](https://abdelrahmannasr.github.io/yadflow/)
7
9
 
8
10
  > πŸ“– **Start here: the [Yadflow Terminology & Workflow Structure Report](https://abdelrahmannasr.github.io/yadflow/)** β€”
@@ -70,12 +72,18 @@ The module ships a zero-dependency CLI, published to npm as
70
72
  [`yadflow`](https://www.npmjs.com/package/yadflow). Run it
71
73
  with `npx` from your **product hub** repo β€” no clone needed.
72
74
 
75
+ > **Platform support.** Linux and macOS are first-class β€” the test suite, the bash check gates, and
76
+ > the end-to-end harness all run on both in CI. The CLI shells out to `git` (and the bash gate
77
+ > scripts), so on **Windows use [WSL](https://learn.microsoft.com/windows/wsl/)**; native PowerShell
78
+ > is not yet supported. Requires **Node.js β‰₯ 18**.
79
+
73
80
  | Command | What it does |
74
81
  |---------|--------------|
75
82
  | `npx yadflow setup` | Guided first-run wizard (the steps below). |
76
83
  | `npx yadflow check` | Read-only report: what is **missing** / **outdated** (drifted) / **stale** (code-context) / **legacy** (pre-2.0 `sdlc-*` names) vs the bundled manifest. |
77
84
  | `npx yadflow check --fix` | Reconcile: fill what is missing **and** update what changed β€” touches nothing already correct. |
78
85
  | `npx yadflow update` | Apply drift only (alias for `check --fix --scope=changed`). Also migrates a pre-2.0 install in place: `sdlc-*` skill copies and marker-owned `sdlc-*.yml` CI files are replaced by their `yad-*` names (a same-named file *you* authored is never touched). |
86
+ | `npx yadflow doctor [--json]` | Environment + state health: tools on PATH and platform auth, config files parse and point at real repos, every epic ledger loads. Exit 1 on any failure; `--json` for CI and bug reports. |
79
87
  | `yad gate open <epic> <artifact>` | Open the front-half **review PR/MR** for an artifact and mark the step `in_review`. |
80
88
  | `yad gate sync <epic> [artifact]` | Pull the PR/MR's reviews + comment threads into the file ledger; **auto-advance** the step when approvals are satisfied, all threads are resolved, and the PR is merged. |
81
89
  | `yad gate comments <epic> [artifact]` | Fetch the unresolved review comments to address (then reply on the PR; reviewers resolve their threads). |
@@ -114,16 +122,18 @@ a manual `yad gate sync` racing CI, or GitLab pipelines β€” two simultaneous syn
114
122
  *commits* via the rebase retry but each works from the state it read at start, so the rarer of two
115
123
  simultaneous advancements can be lost; the next event or scheduled sweep re-syncs and converges.
116
124
 
117
- ### What `setup` walks you through (7 steps)
125
+ ### What `setup` walks you through (8 steps)
118
126
 
119
127
  1. **Preflight** β€” confirm the hub is a git repo (offers `git init`); check `git`/`node`/`npx`.
120
- 2. **Install the module** β€” copy all 17 `yad-*` skills into the IDE skill dirs you pick
128
+ 2. **Install the module** β€” copy all 18 `yad-*` skills into the IDE skill dirs you pick
121
129
  (`.claude/`, `.agents/`, `.zencoder/`, `.opencode/`) and register `_bmad/sdlc/`.
122
130
  3. **Hub platform & roster** β€” detect GitHub/GitLab from the remote; record reviewers β†’ `.sdlc/hub.json`.
123
- 4. **Connect code repos** β€” register each repo into `.sdlc/repos.json` and cache a Repomix pack.
124
- 5. **Wire each repo** β€” CI gates, PR/MR template, and review-comment scaffold.
125
- 6. **AI review** β€” optionally write `.coderabbit.yaml`.
126
- 7. **Done** β€” stamp `.sdlc/cli-version.json` and hand off the AI-only steps (code-maps; first epic).
131
+ 4. **Connect a design tool** β€” record the design tool (Figma / pencil / none) β†’ `.sdlc/design.json` so
132
+ 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).
127
137
 
128
138
  The deterministic file work runs automatically; the AI-only steps are handed to the Claude Code skills
129
139
  with a printed next-action. Re-run `… check --fix` any time the workflow updates β€” it never re-asks for
@@ -143,7 +153,27 @@ provenance). See [`RELEASING.md`](RELEASING.md).
143
153
  > ships the `CHANGELOG.md` in the tarball, and cuts a GitHub release. No manual `npm publish`. See
144
154
  > [`RELEASING.md`](RELEASING.md).
145
155
 
146
- ## Agent skills (all 17)
156
+ ### Troubleshooting (`yad doctor` + error codes)
157
+
158
+ When something is off, run `yad doctor` first β€” it checks the environment (git, gh/glab auth, node
159
+ version), the project state (`.sdlc/*.json` parse and point at real repos), and every epic ledger,
160
+ with a fix-it hint per finding. Failures carry stable, greppable codes, also printed by any failing
161
+ `yad` command:
162
+
163
+ | Code | Meaning | Fix |
164
+ |------|---------|-----|
165
+ | `YAD-ENV-001` | git is not installed or not on PATH | install git β€” every yad command needs it |
166
+ | `YAD-ENV-002` | platform CLI (gh/glab) missing or not authenticated | install it and authenticate β€” `gh auth login` (GitHub) or `glab auth login` (GitLab); the gate degrades to file-only without it |
167
+ | `YAD-ENV-003` | Node.js older than the supported range | install Node >= 18 |
168
+ | `YAD-STATE-001` | a ledger/config JSON file exists but does not parse | fix the file or restore from git β€” never delete a ledger blindly |
169
+ | `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) |
170
+ | `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
+ | `YAD-CFG-001` | `hub.json` names an unknown platform | expected `github`, `gitlab`, or `null` β€” fix it or re-run `yad setup` |
172
+ | `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` |
173
+
174
+ Filing a bug? Attach `yad doctor --json` β€” it contains no secrets (names, paths, and check results only).
175
+
176
+ ## Agent skills (all 18)
147
177
 
148
178
  The CLI **installs and wires** the module; the skills below are the **agents you invoke by name** in your
149
179
  AI IDE (e.g. *β€œrun `yad-epic`”*) to actually do the work. State lives in files you can also edit
@@ -156,6 +186,11 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
156
186
  `.sdlc/repos.json`, then caches an AI-readable picture of each β€” a compressed Repomix pack and a
157
187
  lightweight code-map (existing endpoints/events/data-models/modules), secret-scanned. Idempotent and
158
188
  refreshable; staleness tracked by HEAD sha.
189
+ - **`yad-connect-design`** β€” Connects a design tool (Figma-first, pluggable) so the UI step can
190
+ materialize the actual feature design (mobile screens / web pages) inside it, alongside the Markdown.
191
+ Records the tool + project/file references in `.sdlc/design.json` (local-user / MCP-session auth, no
192
+ stored tokens), detecting the design-tool MCP and degrading to markdown-only when absent. Idempotent
193
+ and refreshable; one connection per project.
159
194
 
160
195
  ### Front half β€” author the "thinking" (once per epic, human-gated)
161
196
 
@@ -170,7 +205,9 @@ directly. Each skill stops at a gate and never auto-advances unless a step has *
170
205
  `.sdlc/contract-lock.json`. Reads `epic.md`; escalates on the contract risk tag.
171
206
  - **`yad-ui`** β€” Front state 5. With the ux-designer, author `ui-design.md` and `DESIGN.md`,
172
207
  driving Impeccable as harness slash-commands (document/extract/craft) when installed, or authoring
173
- directly when not. Reads epic + architecture.
208
+ directly when not. When a design tool is connected (`yad-connect-design`), also **materializes the
209
+ feature design** β€” mobile screens / web pages β€” in the tool (generate or link), recording the
210
+ screen→frame map in `design-links.json`; degrades to markdown-only otherwise. Reads epic + architecture.
174
211
  - **`yad-stories`** β€” Front state 7. With the pm, break the approved epic into user stories, each
175
212
  tagged with the repos that must implement it. Assigns zero-padded `EP-<slug>-S0N` IDs, one file per
176
213
  story under `stories/`. Reads epic + architecture + contract + UI.
package/bin/yad.mjs CHANGED
@@ -9,6 +9,7 @@ import { isValidEpicId } from '../cli/epic-state.mjs';
9
9
  import { runCommit } from '../cli/commit.mjs';
10
10
  import { runOpenPr } from '../cli/openpr.mjs';
11
11
  import { runRepo } from '../cli/repo.mjs';
12
+ import { runDoctor } from '../cli/doctor.mjs';
12
13
 
13
14
  const HELP = `${c.bold('yad')} β€” setup, review-gate & build helpers for the SDLC Workflow module ${c.dim('v' + VERSION)}
14
15
 
@@ -18,6 +19,8 @@ ${c.bold('Setup & maintenance')}
18
19
  yad check --fix Reconcile: fill what is missing, update what changed
19
20
  yad update Apply drift only (alias for: check --fix --scope=changed);
20
21
  also migrates pre-2.0 sdlc-* installs to the yad-* names
22
+ yad doctor [--json] Environment + state health: tools/auth, config files,
23
+ repo paths, epic ledgers (exit 1 on any failure)
21
24
 
22
25
  ${c.bold('Review gate (front half)')}
23
26
  yad gate open <epic> <artifact> Open the review PR/MR; mark the step in_review
@@ -62,6 +65,7 @@ function parseArgs(argv) {
62
65
  else if (a === '--contract-change') o.contractChange = true;
63
66
  else if (a === '--no-push') o.noPush = true;
64
67
  else if (a === '--dry-run') o.dryRun = true;
68
+ else if (a === '--json') o.json = true;
65
69
  else if (a === '-h' || a === '--help') o.help = true;
66
70
  else if (a === '-v' || a === '--version') o.version = true;
67
71
  else if (a.startsWith('--scope=')) o.scope = a.slice('--scope='.length);
@@ -96,6 +100,9 @@ async function main() {
96
100
  case 'update':
97
101
  await reconcile(o.dir, { fix: true, scope: 'changed', force: o.force, today });
98
102
  break;
103
+ case 'doctor':
104
+ await runDoctor(o.dir, { json: o.json });
105
+ break;
99
106
  case 'gate': {
100
107
  const [, action, epic, artifact] = o._;
101
108
  // `gate ci` takes no positionals β€” epic/artifact come from --branch (or a sweep of all PRs).
@@ -130,7 +137,10 @@ async function main() {
130
137
 
131
138
  main()
132
139
  .catch((err) => {
133
- log(c.red(`\nyad failed: ${err?.message || err}`));
140
+ const code = err?.code && /^YAD-/.test(err.code) ? ` [${err.code}]` : '';
141
+ log(c.red(`\nyad failed${code}: ${err?.message || err}`));
142
+ if (err?.hint) log(c.yellow(` β†’ ${err.hint}`));
143
+ if (code) log(c.dim(' (see README "Troubleshooting" for this code, or run `yad doctor`)'));
134
144
  process.exitCode = 1;
135
145
  })
136
146
  .finally(closePrompts);
package/cli/doctor.mjs ADDED
@@ -0,0 +1,166 @@
1
+ // `yad doctor` β€” environment + state health, the complement of `yad check` (file drift).
2
+ // Three sections: environment (tools on PATH, auth), project state (config files parse and
3
+ // point at real repos), epics (each ledger loads). Pure reporting: exit 1 on any FAIL,
4
+ // 0 with warnings. `--json` emits the checks for CI / bug reports.
5
+ import path from 'node:path';
6
+ import fs from 'node:fs';
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';
9
+ import { loadLedger, epicRoot } from './epic-state.mjs';
10
+ import { gitHead } from './setup.mjs';
11
+ import { cliFor } from './platform.mjs';
12
+
13
+ const MIN_NODE = 18;
14
+
15
+ // Each check: { id, section, status: 'ok'|'warn'|'fail', message, hint? }
16
+ function check(checks, id, section, status, message, hint = '') {
17
+ checks.push({ id, section, status, message, ...(hint ? { hint } : {}) });
18
+ }
19
+
20
+ export function envChecks(checks) {
21
+ const major = Number(process.versions.node.split('.')[0]);
22
+ if (major >= MIN_NODE) check(checks, 'node', 'environment', 'ok', `node ${process.versions.node}`);
23
+ else check(checks, 'node', 'environment', 'fail', `node ${process.versions.node} is below the supported range [YAD-ENV-003]`, `install Node.js >= ${MIN_NODE}`);
24
+
25
+ if (has('git')) check(checks, 'git', 'environment', 'ok', 'git present');
26
+ else check(checks, 'git', 'environment', 'fail', 'git not found on PATH [YAD-ENV-001]', 'install git β€” every yad command needs it');
27
+
28
+ for (const tool of ['npx', 'bash']) {
29
+ if (has(tool)) check(checks, tool, 'environment', 'ok', `${tool} present`);
30
+ else check(checks, tool, 'environment', 'warn', `${tool} not found on PATH`, tool === 'npx' ? 'repomix packing will be skipped' : 'the check gates are bash scripts');
31
+ }
32
+ }
33
+
34
+ export function projectChecks(checks, root) {
35
+ const hubPath = path.join(root, PROJECT_FILES.hubConfig);
36
+ const regPath = path.join(root, PROJECT_FILES.reposRegistry);
37
+ const verPath = path.join(root, PROJECT_FILES.version);
38
+ if (!exists(hubPath) && !exists(regPath) && !exists(verPath)) {
39
+ check(checks, 'project', 'project', 'warn', 'no yad project here (.sdlc/ not initialised)', 'run `yad setup` to start one β€” environment checks above still apply');
40
+ return null;
41
+ }
42
+
43
+ // version stamp
44
+ const ver = readJSON(verPath, null);
45
+ if (!ver) check(checks, 'cli-version', 'project', 'warn', `${PROJECT_FILES.version} missing or unreadable`, 'run `yad check --fix`');
46
+ else if (ver.version !== VERSION) check(checks, 'cli-version', 'project', 'warn', `project stamped v${ver.version}, CLI is v${VERSION}`, 'run `yad update` to reconcile');
47
+ else check(checks, 'cli-version', 'project', 'ok', `version stamp matches (v${VERSION})`);
48
+
49
+ // hub.json: parse + shape
50
+ let hub = null;
51
+ if (!exists(hubPath)) {
52
+ check(checks, 'hub', 'project', 'warn', `${PROJECT_FILES.hubConfig} absent β€” file-only gate`, 'run `yad setup` to configure a platform + roster');
53
+ } else {
54
+ let hubBroken = false;
55
+ try {
56
+ hub = readJSONStrict(hubPath, null);
57
+ } catch (e) {
58
+ hubBroken = true;
59
+ check(checks, 'hub', 'project', 'fail', `${PROJECT_FILES.hubConfig} does not parse [${e.code || 'YAD-STATE-001'}]`, e.hint || 'fix the JSON or restore it from git');
60
+ }
61
+ if (hubBroken) { /* reported above */ }
62
+ else if (typeof hub !== 'object' || Array.isArray(hub) || hub === null) check(checks, 'hub', 'project', 'fail', `${PROJECT_FILES.hubConfig} has the wrong shape [YAD-STATE-002]`, 'expected a JSON object');
63
+ else if (![null, undefined, 'github', 'gitlab'].includes(hub.platform)) check(checks, 'hub', 'project', 'fail', `${PROJECT_FILES.hubConfig}: unknown platform '${hub.platform}' [YAD-CFG-001]`, 'expected github, gitlab, or null');
64
+ // Mirror gate.mjs's roster shape check so doctor never reports "ok" on a hub the gate would reject.
65
+ else if (hub.roster !== undefined && !Array.isArray(hub.roster)) check(checks, 'hub', 'project', 'fail', `${PROJECT_FILES.hubConfig}: \`roster\` must be an array [YAD-STATE-002]`, 'fix the file or re-run `yad setup`');
66
+ else {
67
+ check(checks, 'hub', 'project', 'ok', `hub: ${hub.platform || 'file-only'}, ${(hub.roster || []).length} reviewer(s)`);
68
+ // platform CLI + auth (best-effort; auth probing is the user's own session)
69
+ const cli = cliFor(hub.platform);
70
+ if (cli) {
71
+ if (!has(cli)) check(checks, 'platform-cli', 'project', 'warn', `${cli} not found on PATH [YAD-ENV-002]`, `install ${cli} β€” the gate degrades to file-only without it`);
72
+ else if (!run(cli, ['auth', 'status']).ok) check(checks, 'platform-cli', 'project', 'warn', `${cli} present but not authenticated [YAD-ENV-002]`, `run \`${cli} auth login\``);
73
+ else check(checks, 'platform-cli', 'project', 'ok', `${cli} present and authenticated`);
74
+ }
75
+ }
76
+ }
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_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');
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
+ // repos.json: parse + every entry is a live git repo; staleness vs syncedHead
99
+ let registry = { repos: [] };
100
+ let regBroken = false;
101
+ try {
102
+ registry = readJSONStrict(regPath, { repos: [] });
103
+ } catch (e) {
104
+ regBroken = true;
105
+ check(checks, 'repos', 'project', 'fail', `${PROJECT_FILES.reposRegistry} does not parse [${e.code || 'YAD-STATE-001'}]`, e.hint || 'fix the JSON or restore it from git');
106
+ }
107
+ if (regBroken) { /* reported above */ }
108
+ else if (!Array.isArray(registry?.repos)) check(checks, 'repos', 'project', 'fail', `${PROJECT_FILES.reposRegistry} has the wrong shape [YAD-STATE-002]`, 'expected a `repos` array');
109
+ else {
110
+ for (const repo of registry.repos) {
111
+ // A missing/empty path must NOT fall back to the project root (which is itself a git repo and
112
+ // would read as "healthy") β€” an entry with no path is malformed.
113
+ if (!repo.path) { check(checks, `repo:${repo.name || '(unnamed)'}`, 'project', 'fail', `${repo.name || '(unnamed)'}: no \`path\` in repos.json [YAD-STATE-003]`, 're-connect the repo (`yad setup`)'); continue; }
114
+ const repoRoot = path.resolve(root, repo.path);
115
+ if (!exists(repoRoot)) { check(checks, `repo:${repo.name}`, 'project', 'fail', `${repo.name}: path ${repo.path} does not exist [YAD-STATE-003]`, 'fix the path in repos.json or re-connect the repo'); continue; }
116
+ const head = gitHead(repoRoot);
117
+ if (!head) { check(checks, `repo:${repo.name}`, 'project', 'fail', `${repo.name}: ${repo.path} is not a git repository (or has no commits) [YAD-STATE-003]`, 'init/clone the repo, then re-connect it'); continue; }
118
+ if (repo.syncedHead && head !== repo.syncedHead) check(checks, `repo:${repo.name}`, 'project', 'warn', `${repo.name}: code-context is stale (HEAD moved since last pack)`, 'run `yad repo refresh ' + repo.name + '`');
119
+ else check(checks, `repo:${repo.name}`, 'project', 'ok', `${repo.name}: git repo, context fresh`);
120
+ }
121
+ if (!registry.repos.length) check(checks, 'repos', 'project', 'warn', 'no code repos registered', 'run `yad setup` to connect one');
122
+ }
123
+ return { hub, registry };
124
+ }
125
+
126
+ export function epicChecks(checks, root) {
127
+ const epicsDir = path.join(root, 'epics');
128
+ if (!exists(epicsDir)) return;
129
+ for (const e of fs.readdirSync(epicsDir).sort()) {
130
+ if (!fs.statSync(path.join(epicsDir, e)).isDirectory()) continue;
131
+ try {
132
+ const ledger = loadLedger(epicRoot(root, e));
133
+ if (!ledger.state) check(checks, `epic:${e}`, 'epics', 'warn', `${e}: no state.json β€” epic not seeded`, 'author it via yad-epic, or remove the directory');
134
+ else check(checks, `epic:${e}`, 'epics', 'ok', `${e}: currentStep ${ledger.state.currentStep}`);
135
+ } catch (err) {
136
+ check(checks, `epic:${e}`, 'epics', 'fail', `${e}: ${err.message} [${err.code || 'YAD-STATE-001'}]`, err.hint || 'fix the file or restore it from git');
137
+ }
138
+ }
139
+ }
140
+
141
+ export async function runDoctor(root, { json = false } = {}) {
142
+ const checks = [];
143
+ envChecks(checks);
144
+ projectChecks(checks, root);
145
+ epicChecks(checks, root);
146
+
147
+ const failed = checks.filter((x) => x.status === 'fail');
148
+ const warned = checks.filter((x) => x.status === 'warn');
149
+ if (json) {
150
+ log(JSON.stringify({ version: VERSION, ok: failed.length === 0, checks }, null, 2));
151
+ } else {
152
+ log(c.bold(`\nyad doctor ${c.dim('v' + VERSION)}`));
153
+ let section = '';
154
+ for (const x of checks) {
155
+ if (x.section !== section) { section = x.section; log(`\n ${c.bold(section)}`); }
156
+ ({ ok, warn, fail })[x.status](x.message);
157
+ if (x.hint && x.status !== 'ok') hand(x.hint);
158
+ }
159
+ log('');
160
+ if (failed.length) fail(`${failed.length} problem(s) found`);
161
+ else if (warned.length) info(`healthy with ${warned.length} warning(s)`);
162
+ else ok('all clear');
163
+ }
164
+ if (failed.length) process.exitCode = 1;
165
+ return { ok: failed.length === 0, failed: failed.length, warned: warned.length, checks };
166
+ }
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import { createHash } from 'node:crypto';
6
6
  import fs from 'node:fs';
7
7
  import { readJSONStrict, writeJSON, fileSha } from './lib.mjs';
8
+ import { err } from './errors.mjs';
8
9
  import { epicFiles } from './manifest.mjs';
9
10
 
10
11
  const RISK_ESCALATORS = ['contract', 'auth', 'payments'];
@@ -98,7 +99,7 @@ export function artifactHash(epicDir, artifact) {
98
99
 
99
100
  // Shape checks for the ledger files. Fail fast with the exact file named β€” a wrong-shape ledger
100
101
  // silently treated as a default would be rewritten by the next sync, destroying the real data.
101
- const badShape = (file, what) => new Error(`${file}: ${what} β€” fix the file or restore it from git`);
102
+ const badShape = (file, what) => err('YAD-STATE-002', `${file}: ${what}`, 'fix the file or restore it from git');
102
103
  function requireArray(v, file) {
103
104
  if (!Array.isArray(v)) throw badShape(file, 'expected a JSON array');
104
105
  return v;
package/cli/errors.mjs ADDED
@@ -0,0 +1,26 @@
1
+ // Structured error codes for the `yad` CLI. A YadError carries a stable code (greppable,
2
+ // documented in README "Troubleshooting") and a one-line recovery hint that the top-level
3
+ // catch in bin/yad.mjs prints after the message. Plain Errors still work everywhere; codes
4
+ // are reserved for the failures users actually hit and need to act on.
5
+ export class YadError extends Error {
6
+ constructor(code, message, hint = '') {
7
+ super(message);
8
+ this.name = 'YadError';
9
+ this.code = code;
10
+ this.hint = hint;
11
+ }
12
+ }
13
+
14
+ // The catalog β€” single source for doctor, the top-level catch, and the README table.
15
+ export const CODES = {
16
+ 'YAD-ENV-001': 'git is not installed or not on PATH',
17
+ 'YAD-ENV-002': 'the platform CLI (gh/glab) is missing or not authenticated',
18
+ 'YAD-ENV-003': 'Node.js is older than the supported range (>=18)',
19
+ 'YAD-STATE-001': 'a ledger/config JSON file exists but does not parse',
20
+ 'YAD-STATE-002': 'a ledger/config JSON file parses but has the wrong shape',
21
+ 'YAD-STATE-003': 'a registered repo path is missing or not a git repository',
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
+ };
25
+
26
+ export const err = (code, message, hint) => new YadError(code, message, hint);
package/cli/gate.mjs CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  artifactPaths, upsertHubPr,
15
15
  } from './epic-state.mjs';
16
16
  import { readPr, mapApprovers, createPr } from './platform.mjs';
17
+ import { err } from './errors.mjs';
17
18
 
18
19
  // ---- tiny frontmatter reader (key: value, and `repos: [a, b]`) ----------------------------------
19
20
  function frontmatter(file) {
@@ -64,18 +65,23 @@ function warnUnlockedContract(epicDir, artifact) {
64
65
  function loadHub(root) {
65
66
  const hubFile = path.join(root, PROJECT_FILES.hubConfig);
66
67
  const regFile = path.join(root, PROJECT_FILES.reposRegistry);
68
+ // Distinguish an ABSENT hub.json (null default β†’ fine, file-only gate) from one that exists but
69
+ // holds literal `null` (malformed β€” must not silently downgrade to file-only).
67
70
  const hub = readJSONStrict(hubFile, null);
71
+ if (hub === null && fs.existsSync(hubFile)) {
72
+ throw err('YAD-STATE-002', `${hubFile}: contains \`null\` β€” expected a config object`, 'fix the file or re-run `yad setup`');
73
+ }
68
74
  if (hub !== null) {
69
- if (typeof hub !== 'object' || Array.isArray(hub)) throw new Error(`${hubFile}: expected a JSON object`);
75
+ if (typeof hub !== 'object' || Array.isArray(hub)) throw err('YAD-STATE-002', `${hubFile}: expected a JSON object`, 'fix the file or re-run `yad setup`');
70
76
  if (![null, undefined, 'github', 'gitlab'].includes(hub.platform)) {
71
- throw new Error(`${hubFile}: unknown platform '${hub.platform}' β€” expected github, gitlab, or null`);
77
+ throw err('YAD-CFG-001', `${hubFile}: unknown platform '${hub.platform}'`, 'expected github, gitlab, or null β€” fix the file or re-run `yad setup`');
72
78
  }
73
79
  if (hub.roster !== undefined && !Array.isArray(hub.roster)) {
74
- throw new Error(`${hubFile}: expected \`roster\` to be an array`);
80
+ throw err('YAD-STATE-002', `${hubFile}: expected \`roster\` to be an array`, 'fix the file or re-run `yad setup`');
75
81
  }
76
82
  }
77
83
  const registry = readJSONStrict(regFile, { repos: [] });
78
- if (!Array.isArray(registry?.repos)) throw new Error(`${regFile}: expected a \`repos\` array`);
84
+ if (!Array.isArray(registry?.repos)) throw err('YAD-STATE-002', `${regFile}: expected a \`repos\` array`, 'fix the file or re-run `yad setup`');
79
85
  return { hub, repos: registry.repos };
80
86
  }
81
87
 
@@ -131,7 +137,7 @@ function writeComments(epicDir, base, today, blocking) {
131
137
  // Upsert machine-readable participation records into the comments ledger (the counterpart to the
132
138
  // markdown side file) so the ledger β€” not just reviews/*.md β€” reflects platform thread state. One
133
139
  // record per (step, commenter, round); `round` is the count of prior synced rounds for the step.
134
- function recordComments(comments, { artifact, stepId, today, roster, repos, blocking }) {
140
+ function recordComments(comments, { artifact, stepId, today, roster, blocking }) {
135
141
  if (!blocking.length) return comments;
136
142
  const byName = (login) => (roster.find((r) => r.login === login)?.name) || login || 'reviewer';
137
143
  const roleOf = (login) => (roster.find((r) => r.login === login)?.role) || 'reviewer';
@@ -386,8 +392,8 @@ export async function gateStatus(root, { epic } = {}) {
386
392
  }
387
393
  }
388
394
 
389
- export async function gateOpen(root, { epic, artifact, today } = {}) {
390
- const { hub, repos } = loadHub(root);
395
+ export async function gateOpen(root, { epic, artifact } = {}) {
396
+ const { hub } = loadHub(root);
391
397
  const epicDir = epicRoot(root, epic);
392
398
  const ledger = loadLedger(epicDir);
393
399
  if (!ledger.state) { fail(`no epic state at ${epicDir}`); process.exitCode = 1; return; }
package/cli/lib.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  // Shared helpers for the `yad` CLI. Node >=18 built-ins only β€” no dependencies.
2
2
  import { createHash } from 'node:crypto';
3
+ import { err } from './errors.mjs';
3
4
  import { spawnSync } from 'node:child_process';
4
5
  import * as readline from 'node:readline/promises';
5
6
  import { stdin as input, stdout as output } from 'node:process';
@@ -108,7 +109,7 @@ export function readJSONStrict(p, def = null) {
108
109
  try {
109
110
  return JSON.parse(fs.readFileSync(p, 'utf8'));
110
111
  } catch (e) {
111
- throw new Error(`corrupt JSON in ${p}: ${e.message} β€” fix or delete the file`);
112
+ throw err('YAD-STATE-001', `corrupt JSON in ${p}: ${e.message}`, 'fix the file or restore it from git β€” never delete a ledger blindly');
112
113
  }
113
114
  }
114
115
  // Atomic: serialize first, write a sibling tmp file (same dir = same filesystem),
package/cli/manifest.mjs CHANGED
@@ -10,7 +10,7 @@ import { readFileSync } from 'node:fs';
10
10
  const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
11
11
  export const VERSION = version;
12
12
 
13
- // The 17 hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
13
+ // The 18 hand-authored yad-* skills (mirrors skills/sdlc/install.sh).
14
14
  export const SKILLS = [
15
15
  'yad-analysis',
16
16
  'yad-epic',
@@ -18,6 +18,7 @@ export const SKILLS = [
18
18
  'yad-ui',
19
19
  'yad-stories',
20
20
  'yad-connect-repos',
21
+ 'yad-connect-design',
21
22
  'yad-spec',
22
23
  'yad-implement',
23
24
  'yad-checks',
@@ -79,10 +80,18 @@ export const IDE_OPENCODE_DIR = '.opencode/commands'; // <skill>.md (flat SKILL.
79
80
  // Module registration files copied from skills/sdlc/ into _bmad/sdlc/.
80
81
  export const MODULE_FILES = ['config.yaml', 'module-help.csv'];
81
82
 
83
+ // Supported design-tool adapters (mirrors skills/sdlc/config.yaml `design.tools`); `DESIGN_PRIMARY` is
84
+ // the fallback `registerDesign`/setup use when an unknown tool is named, and `none` is the explicit
85
+ // markdown-only choice. (doctor does NOT fall back β€” an unknown tool there is a hard YAD-CFG-002 fail,
86
+ // mirroring how registerRepo falls back on platform while doctor fails on an unknown hub platform.)
87
+ export const DESIGN_TOOLS = ['figma', 'pencil'];
88
+ export const DESIGN_PRIMARY = 'figma';
89
+
82
90
  // Project-level files setup produces (used by `check` to spot missing setup).
83
91
  export const PROJECT_FILES = {
84
92
  reposRegistry: '.sdlc/repos.json',
85
93
  hubConfig: '.sdlc/hub.json',
94
+ designConfig: '.sdlc/design.json',
86
95
  version: '.sdlc/cli-version.json',
87
96
  };
88
97
 
package/cli/reconcile.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // manifest: missing setup, drifted files, stale code-context.
4
4
  import path from 'node:path';
5
5
  import {
6
- c, log, ok, info, warn, hand, fail, readJSON, writeJSON, exists,
6
+ c, log, ok, info, warn, hand, readJSON, writeJSON, exists,
7
7
  } from './lib.mjs';
8
8
  import { VERSION, PROJECT_FILES } from './manifest.mjs';
9
9
  import {
package/cli/setup.mjs CHANGED
@@ -3,9 +3,9 @@ import path from 'node:path';
3
3
  import fs from 'node:fs';
4
4
  import {
5
5
  c, log, step, ok, info, warn, hand, fail, ask, askYesNo, run, has,
6
- exists, asset, copyFile, readJSON, writeJSON,
6
+ exists, readJSON, writeJSON,
7
7
  } from './lib.mjs';
8
- import { VERSION, IDE_FOLDER_TARGETS, IDE_OPENCODE_DIR, PROJECT_FILES } from './manifest.mjs';
8
+ import { VERSION, IDE_FOLDER_TARGETS, PROJECT_FILES, DESIGN_TOOLS, DESIGN_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,35 @@ 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
+
61
90
  function applyActions(actions, { force = false } = {}) {
62
91
  let changed = 0;
63
92
  for (const a of actions) {
@@ -71,7 +100,7 @@ function applyActions(actions, { force = false } = {}) {
71
100
  }
72
101
 
73
102
  export async function runSetup(root, opts = {}) {
74
- const total = 7;
103
+ const total = 8;
75
104
  log(c.bold(`\nSDLC Workflow setup ${c.dim('v' + VERSION)}`));
76
105
  log(c.dim(`target: ${root}`));
77
106
 
@@ -131,8 +160,26 @@ export async function runSetup(root, opts = {}) {
131
160
  ok(`wrote ${PROJECT_FILES.hubConfig} (${roster.length} reviewer(s))`);
132
161
  }
133
162
 
134
- // 4. Connect code repos
135
- step(4, total, 'Connect code repos');
163
+ // 4. Connect a design tool (Figma-first, pluggable; the UI step materializes the design here)
164
+ step(4, total, 'Connect a design tool (Figma / pencil / none)');
165
+ const designPath = path.join(root, PROJECT_FILES.designConfig);
166
+ if (exists(designPath) && !(await askYesNo('design.json exists β€” reconfigure?', false))) {
167
+ info('keeping existing .sdlc/design.json');
168
+ } else {
169
+ let tool = (await ask(`Design tool (${DESIGN_TOOLS.join('/')}/none)`, DESIGN_PRIMARY)).toLowerCase();
170
+ if (![...DESIGN_TOOLS, 'none'].includes(tool)) {
171
+ warn(`unknown design tool '${tool}' β€” using ${DESIGN_PRIMARY}`);
172
+ tool = DESIGN_PRIMARY;
173
+ }
174
+ const project_url = tool === 'none' ? null : (await ask(' project/file URL (blank to set later)', '')) || null;
175
+ registerDesign(root, { tool, project_url, today: opts.today ?? null });
176
+ ok(tool === 'none'
177
+ ? `wrote ${PROJECT_FILES.designConfig} (markdown-only)`
178
+ : `wrote ${PROJECT_FILES.designConfig} (${tool})`);
179
+ }
180
+
181
+ // 5. Connect code repos
182
+ step(5, total, 'Connect code repos');
136
183
  const regPath = path.join(root, PROJECT_FILES.reposRegistry);
137
184
  const registry = readJSON(regPath, { repos: [] });
138
185
  const known = new Set(registry.repos.map((r) => r.name));
@@ -155,8 +202,8 @@ export async function runSetup(root, opts = {}) {
155
202
  }
156
203
  }
157
204
 
158
- // 5. Wire each connected repo + the hub itself
159
- step(5, total, 'Wire connected repos + the hub (CI gates, PR template, comment scaffold, gate-sync)');
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)');
160
207
  if (registry.repos.length === 0) info('no repos to wire');
161
208
  for (const repo of registry.repos) {
162
209
  log(` ${c.bold(repo.name)} ${c.dim(`(${repo.platform})`)}`);
@@ -171,8 +218,8 @@ export async function runSetup(root, opts = {}) {
171
218
  // author allowlists for the verified-commits gate (hub + every repo), from the roster emails
172
219
  applyActions(authorsActions(root, registry.repos), { force: true });
173
220
 
174
- // 6. Optional CodeRabbit
175
- step(6, total, 'AI review (CodeRabbit)');
221
+ // 7. Optional CodeRabbit
222
+ step(7, total, 'AI review (CodeRabbit)');
176
223
  for (const repo of registry.repos) {
177
224
  const cr = path.join(path.resolve(root, repo.path), '.coderabbit.yaml');
178
225
  if (exists(cr)) { info(`${repo.name}: .coderabbit.yaml present`); continue; }
@@ -182,13 +229,17 @@ export async function runSetup(root, opts = {}) {
182
229
  }
183
230
  }
184
231
 
185
- // 7. Summary + version stamp
186
- step(7, total, 'Done');
232
+ // 8. Summary + version stamp
233
+ step(8, total, 'Done');
187
234
  writeJSON(path.join(root, PROJECT_FILES.version), { version: VERSION, ideTargets, updatedAt: opts.today ?? null });
188
235
  ok(`stamped ${PROJECT_FILES.version} (v${VERSION})`);
189
236
  log('');
190
237
  log(c.bold('Next β€” AI-only steps (run in Claude Code):'));
191
238
  hand('generate code-maps: run `yad-connect-repos` for each connected repo');
239
+ const design = readJSON(designPath, null);
240
+ if (design && design.tool && design.tool !== 'none') {
241
+ hand(`confirm the design tool: run \`yad-connect-design\` to detect the ${design.tool} MCP (or it degrades to markdown-only)`);
242
+ }
192
243
  hand('author your first epic: run `yad-epic`');
193
244
  log('');
194
245
  log(c.dim('Re-run anytime: `yad check` (report) / `yad check --fix` (reconcile).'));
package/docs/index.html CHANGED
@@ -284,12 +284,14 @@
284
284
  <div class="lane">
285
285
  <div class="lane-title">0 Β· One-time setup (team lead, per project)</div>
286
286
  <div class="flow-v">
287
- <div class="node plain"><strong>Install the module</strong><small><code>npx yadflow setup</code> β€” copies the 17 skills into your IDE dirs</small></div>
287
+ <div class="node plain"><strong>Install the module</strong><small><code>npx yadflow setup</code> β€” copies the 18 skills into your IDE dirs</small></div>
288
288
  <div class="arrow-v">β–Ό</div>
289
289
  <div class="node plain"><strong>Wire each repo</strong><small><code>yad-checks</code> Β· <code>yad-pr-template</code> Β· <code>yad-review-comments</code> (CI gates, PR template, comment scaffold)</small></div>
290
290
  <div class="arrow-v">β–Ό</div>
291
291
  <div class="node plain"><strong>Connect code repos</strong><small><code>yad-connect-repos</code> β†’ <code>repos.json</code> + a cached code-map per repo <em>(skip if greenfield)</em></small></div>
292
292
  <div class="arrow-v">β–Ό</div>
293
+ <div class="node plain"><strong>Connect a design tool</strong><small><code>yad-connect-design</code> β†’ <code>design.json</code> (Figma-first, pluggable) so <code>yad-ui</code> can materialize the screens <em>(optional β€” degrades to markdown-only)</em></small></div>
294
+ <div class="arrow-v">β–Ό</div>
293
295
  <div class="node plain"><strong>Optional: hub on a platform</strong><small>detect GitHub/GitLab + reviewer roster, so reviews run on real PRs/MRs</small></div>
294
296
  </div>
295
297
  </div>
@@ -311,7 +313,7 @@
311
313
  <div class="arrow-v">β–Ό</div>
312
314
  <div class="node gate">gate Β· architecture review<small><strong>escalated:</strong> base rule + a domain owner for every repo in the epic</small></div>
313
315
  <div class="arrow-v">β–Ό</div>
314
- <div class="node artifact"><strong>yad-ui</strong><small><code>ui-design.md</code> + <code>DESIGN.md</code></small></div>
316
+ <div class="node artifact"><strong>yad-ui</strong><small><code>ui-design.md</code> + <code>DESIGN.md</code> + the screens in the connected design tool (<code>design-links.json</code>)</small></div>
315
317
  <div class="arrow-v">β–Ό</div>
316
318
  <div class="node gate">gate Β· UI review<small>base rule</small></div>
317
319
  <div class="arrow-v">β–Ό</div>
@@ -367,11 +369,11 @@
367
369
  epic.md the feature, agreed
368
370
  architecture.md how it will be built
369
371
  contract.md the LOCKED shared surface (APIs, events, data model)
370
- ui-design.md + DESIGN.md
372
+ ui-design.md + DESIGN.md (+ the screens in the connected design tool)
371
373
  stories/ one file per story (EP-&lt;slug&gt;-S01.md …)
372
374
  reviews/ reviewer comments, per artifact
373
375
  .sdlc/ state.json Β· approvals.json Β· contract-lock.json
374
- build-state/ Β· trust-log.json Β· build-log.json
376
+ design-links.json Β· build-state/ Β· trust-log.json Β· build-log.json
375
377
 
376
378
  each code repo/
377
379
  specs/&lt;story-id&gt;/ spec, plan, tasks (+ link.md back to the story)
@@ -438,7 +440,7 @@ each code repo/
438
440
  <div class="term"><strong>State machine</strong><span class="badge b-core">core</span><br>The core design: the workflow is a fixed set of <em>states</em> (steps). Each state does its work and waits at a gate. The states never change β€” only the <em>trigger</em> (who moves work forward) changes. That is what lets the team start fully manual and automate gradually without rebuilding anything.</div>
439
441
  <div class="term"><strong>Product hub (product repo)</strong><span class="badge b-core">core</span><br>The git repo that holds the shared β€œthinking”: epics, architecture, the contract, UI design, stories, reviews and all state. The brain of the project. Code never lives here.</div>
440
442
  <div class="term"><strong>Code repo</strong><span class="badge b-core">core</span><br>A separate git repo holding real application code (e.g. backend, mobile, dashboard). Each story’s spec lives inside the code repo it belongs to, with a link back to the story in the hub.</div>
441
- <div class="term"><strong>Skills source (this repo)</strong><span class="badge b-core">core</span><br>The <code>yadflow</code> repo itself β€” where the 17 skills live and where you pull updates from. No real product work happens inside it.</div>
443
+ <div class="term"><strong>Skills source (this repo)</strong><span class="badge b-core">core</span><br>The <code>yadflow</code> repo itself β€” where the 18 skills live and where you pull updates from. No real product work happens inside it.</div>
442
444
  <div class="term"><strong>Front half (β€œdecide” / the brain)</strong><span class="badge b-core">core</span><br>The first half of the workflow, run once per epic in the product hub: analysis (optional) β†’ epic β†’ architecture + contract β†’ UI design β†’ stories β€” each followed by a human review gate. Permanently human-approved.</div>
443
445
  <div class="term"><strong>Back half / build half (β€œbuild”)</strong><span class="badge b-core">core</span><br>The second half, run once per story per code repo, inside that repo: spec β†’ implement β†’ checks β†’ PR β†’ ship. These mechanical steps may earn automation over time.</div>
444
446
  <div class="term"><strong>Gate</strong><span class="badge b-core">core</span><br>A stopping point after a step. The step writes its output and <em>waits</em>; a human must approve before the workflow moves on. β€œEvery step stops at a gate” is the heart of the whole system.</div>
@@ -529,7 +531,7 @@ each code repo/
529
531
  <div class="term"><strong>CodeRabbit</strong><span class="badge b-tool">tools</span><br>An AI code-review service used as the advisory first pass on PRs. Optional; never the merge authority.</div>
530
532
  <div class="term"><strong>Graceful degradation</strong><span class="badge b-tool">tools</span><br>The rule that every optional tool (Spec Kit, Impeccable, Repomix, CodeRabbit) can be absent: the workflow falls back to doing the work directly and <em>records</em> that the tool was missing. You can start with none of them.</div>
531
533
  <div class="term"><strong>Tool-agnostic rule</strong><span class="badge b-tool">tools</span><br>Talk to every tool through its commands and files, never through its internal code β€” so each tool stays swappable.</div>
532
- <div class="term"><strong>Skill</strong><span class="badge b-tool">tools</span><br>One of the 17 named agents (e.g. <code>yad-epic</code>) you invoke by name in your AI IDE. Each skill does one step’s work, writes files, and stops at its gate.</div>
534
+ <div class="term"><strong>Skill</strong><span class="badge b-tool">tools</span><br>One of the 18 named agents (e.g. <code>yad-epic</code>) you invoke by name in your AI IDE. Each skill does one step’s work, writes files, and stops at its gate.</div>
533
535
  <div class="term"><strong>The <code>yad</code> CLI</strong><span class="badge b-tool">tools</span><br>A zero-dependency command-line tool (npm: <code>yadflow</code>) that installs, wires and reconciles the workflow (<code>setup</code>, <code>check --fix</code>, <code>update</code>) and runs the deterministic pieces (<code>gate</code>, <code>commit</code>, <code>open-pr</code>, <code>repo</code>).</div>
534
536
 
535
537
  <h3>People &amp; AI roles</h3>
@@ -672,7 +674,7 @@ each code repo/
672
674
 
673
675
  <!-- ======================================================= -->
674
676
  <section id="skills">
675
- <h2>6. The 17 skills β€” what each does, how to use it, and when</h2>
677
+ <h2>6. The 18 skills β€” what each does, how to use it, and when</h2>
676
678
  <p>A <strong>skill</strong> is an agent you invoke <em>by name</em> in your AI IDE β€” you just ask in
677
679
  plain words, e.g. <em>β€œrun <code>yad-epic</code>”</em>, adding any inputs the skill needs
678
680
  (<code>repo: backend</code>, <code>story: EP-…-S01</code>, <code>action: wire</code>, …).
@@ -682,10 +684,11 @@ each code repo/
682
684
  <table>
683
685
  <tr><th>Skill</th><th>Half</th><th>One line</th></tr>
684
686
  <tr><td><a href="#sk-connect"><code>yad-connect-repos</code></a></td><td>Setup</td><td>Register a code repo with the hub + cache its code-map.</td></tr>
687
+ <tr><td><a href="#sk-connect-design"><code>yad-connect-design</code></a></td><td>Setup</td><td>Connect a design tool (Figma-first) so <code>yad-ui</code> can materialize the screens.</td></tr>
685
688
  <tr><td><a href="#sk-analysis"><code>yad-analysis</code></a></td><td>Front</td><td><em>(Optional)</em> pressure-test an idea into <code>analysis.md</code>.</td></tr>
686
689
  <tr><td><a href="#sk-epic"><code>yad-epic</code></a></td><td>Front</td><td>Start a feature: write <code>epic.md</code>, assign the ID.</td></tr>
687
690
  <tr><td><a href="#sk-arch"><code>yad-architecture</code></a></td><td>Front</td><td>Author <code>architecture.md</code> + the locked <code>contract.md</code>.</td></tr>
688
- <tr><td><a href="#sk-ui"><code>yad-ui</code></a></td><td>Front</td><td>Author <code>ui-design.md</code> + <code>DESIGN.md</code>.</td></tr>
691
+ <tr><td><a href="#sk-ui"><code>yad-ui</code></a></td><td>Front</td><td>Author <code>ui-design.md</code> + <code>DESIGN.md</code>; materialize the screens in the connected design tool.</td></tr>
689
692
  <tr><td><a href="#sk-stories"><code>yad-stories</code></a></td><td>Front</td><td>Break the epic into repo-tagged stories.</td></tr>
690
693
  <tr><td><a href="#sk-gate"><code>yad-review-gate</code></a></td><td>Cross-cutting</td><td>Review / comment / approve / advance <strong>any</strong> gate.</td></tr>
691
694
  <tr><td><a href="#sk-bridge"><code>yad-hub-bridge</code></a></td><td>Cross-cutting</td><td>Run front-half reviews over real PRs/MRs and sync them back.</td></tr>
@@ -715,6 +718,17 @@ yad-connect-repos action: roster login: alice-gh name: alice role: owner # onc
715
718
  <div class="row"><span class="lbl lbl-when">When</span> During one-time setup, and again any time you add a new code repo. Re-run <code>refresh</code> when a skill warns a repo is stale. <strong>Greenfield with no code yet? Skip it entirely</strong> β€” the brain proceeds without it.</div>
716
719
  </div>
717
720
 
721
+ <div class="skillcard" id="sk-connect-design">
722
+ <h4><code>yad-connect-design</code></h4>
723
+ <div class="row"><span class="lbl lbl-what">What</span> Connects a <strong>design tool</strong> to the product hub so the UI step can materialize the <em>actual</em> feature design β€” mobile screens / web pages β€” inside it, alongside the Markdown. <strong>Figma-first but pluggable</strong> (a design-tool adapter; <code>pencil</code> is a second provider). It records the tool + project/file references in <code>design.json</code> through your own authenticated MCP session β€” <strong>no tokens are ever stored</strong> β€” and degrades to markdown-only when no design-tool MCP is available.</div>
724
+ <div class="row"><span class="lbl lbl-how">How</span> Run it in the product hub, once per project:</div>
725
+ <pre><code>yad-connect-design action: connect tool: figma project_url: https://figma.com/files/project/…
726
+ yad-connect-design action: list # show the connection + whether the MCP is available
727
+ yad-connect-design action: refresh # re-detect the MCP (after authenticating a session)
728
+ yad-connect-design action: disconnect</code></pre>
729
+ <div class="row"><span class="lbl lbl-when">When</span> During one-time setup (the wizard records it), and again any time you switch design tools. <strong>No design tool? Skip it</strong> β€” <code>yad-ui</code> simply authors the Markdown, exactly as before.</div>
730
+ </div>
731
+
718
732
  <h3>Front half β€” author the thinking (in the product hub)</h3>
719
733
 
720
734
  <div class="skillcard" id="sk-analysis">
@@ -743,7 +757,7 @@ yad-connect-repos action: roster login: alice-gh name: alice role: owner # onc
743
757
 
744
758
  <div class="skillcard" id="sk-ui">
745
759
  <h4><code>yad-ui</code></h4>
746
- <div class="row"><span class="lbl lbl-what">What</span> With the ux-designer AI, authors the UI design for the epic: <code>ui-design.md</code> (this feature’s screens and flows) + <code>DESIGN.md</code> (the reusable design language). Drives Impeccable’s <code>document / extract / craft</code> commands when installed; writes the files directly when not (and records that).</div>
760
+ <div class="row"><span class="lbl lbl-what">What</span> With the ux-designer AI, authors the UI design for the epic: <code>ui-design.md</code> (this feature’s screens and flows) + <code>DESIGN.md</code> (the reusable design language). Drives Impeccable’s <code>document / extract / craft</code> commands when installed; writes the files directly when not (and records that). When a design tool is connected (<code>yad-connect-design</code>), it also <strong>materializes the screens in that tool</strong> β€” generating them from the spec or linking an existing design β€” and records the screenβ†’frame map in <code>design-links.json</code>; otherwise it stays markdown-only.</div>
747
761
  <div class="row"><span class="lbl lbl-how">How</span> In the product hub, after the architecture gate passes:</div>
748
762
  <pre><code>run yad-ui β€” epic: EP-istifta-inquiries</code></pre>
749
763
  <div class="row"><span class="lbl lbl-when">When</span> After architecture, before stories. Skip-able only if the epic genuinely has no UI. Stops at the <strong>UI review gate</strong> (base rule).</div>
@@ -883,7 +897,7 @@ yad-status EP-istifta-inquiries # one epic in detail</code></pre>
883
897
 
884
898
  <div class="skillcard">
885
899
  <h4><code>npx yadflow setup</code></h4>
886
- <div class="row"><span class="lbl lbl-what">What</span> The guided first-run wizard. Seven steps: preflight (git/node check), install all 17 skills into the IDE dirs you pick, detect the hub’s platform + record the reviewer roster, connect your code repos, wire each one (CI gates, PR template, comment scaffold), optionally write the AI-review config, and stamp the installed version.</div>
900
+ <div class="row"><span class="lbl lbl-what">What</span> The guided first-run wizard. Eight steps: preflight (git/node check), install all 18 skills into the IDE dirs you pick, detect the hub’s platform + record the reviewer roster, connect a design tool (Figma-first; optional), connect your code repos, wire each one (CI gates, PR template, comment scaffold), optionally write the AI-review config, and stamp the installed version.</div>
887
901
  <div class="row"><span class="lbl lbl-how">How</span></div>
888
902
  <pre><code>cd &lt;product-hub-repo&gt;
889
903
  npx yadflow setup</code></pre>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "2.0.1",
3
+ "version": "2.2.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, repo). A BMAD module + 17 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
@@ -20,6 +20,7 @@
20
20
  "bin/",
21
21
  "cli/",
22
22
  "!cli/test.mjs",
23
+ "!cli/test-checks.mjs",
23
24
  "skills/",
24
25
  "docs/index.html",
25
26
  "README.md",
@@ -35,7 +36,10 @@
35
36
  },
36
37
  "scripts": {
37
38
  "yad": "node bin/yad.mjs",
38
- "test": "node --test cli/test.mjs",
39
+ "lint": "eslint cli bin",
40
+ "test": "node --test cli/test.mjs cli/test-checks.mjs",
41
+ "test:e2e": "bash test/e2e/run.sh",
42
+ "coverage": "node --test --experimental-test-coverage --test-coverage-exclude='cli/test*.mjs' --test-coverage-lines=70 --test-coverage-branches=70 cli/test.mjs cli/test-checks.mjs",
39
43
  "diagrams": "npx -y @mermaid-js/mermaid-cli -i docs/diagrams/sdlc-overview.mmd -o docs/diagrams/sdlc-overview.svg -b transparent && npx -y @mermaid-js/mermaid-cli -i docs/diagrams/review-loop.mmd -o docs/diagrams/review-loop.svg -b transparent",
40
44
  "prepublishOnly": "npm test"
41
45
  },
@@ -49,7 +53,9 @@
49
53
  "spec-driven-development"
50
54
  ],
51
55
  "devDependencies": {
56
+ "@eslint/js": "^9.39.4",
52
57
  "@semantic-release/changelog": "^6.0.3",
58
+ "eslint": "^9.39.4",
53
59
  "semantic-release": "^25.0.3"
54
60
  }
55
61
  }
@@ -118,6 +118,22 @@ code_context:
118
118
  # works for both github and gitlab (and self-hosted), and stores NO tokens in the registry.
119
119
  platforms: [github, gitlab]
120
120
 
121
+ # Design tool (yad-connect-design) β€” the UI design step (yad-ui) materializes the actual feature design
122
+ # (mobile screens / web pages) inside a connected design tool, alongside ui-design.md + DESIGN.md. The
123
+ # tool is reached through its MCP (a harness MCP server, like Impeccable's slash-commands β€” NOT a CLI),
124
+ # detected per provider and DEGRADING to markdown-only when absent. Figma-first but PLUGGABLE: a
125
+ # design-tool adapter, like the github/gitlab platform adapter. Connection is one-per-project, recorded
126
+ # at setup; the per-epic screen->frame map (design-links.json) is written by yad-ui per epic.
127
+ design:
128
+ registry: "{project-root}/.sdlc/design.json" # project-wide design connection (NOT per-epic)
129
+ tools: [figma, pencil] # supported adapters; an unknown tool falls back to `primary`
130
+ primary: figma # the default/named provider
131
+ degrade: markdown-only # no tool / no MCP => yad-ui authors ui-design.md + DESIGN.md only
132
+ links: "{project-root}/epics/EP-<slug>/.sdlc/design-links.json" # per-epic screen->frame map (yad-ui)
133
+ # Auth: `yad-connect-design` connects through the LOCAL user's own authenticated MCP session and stores
134
+ # NO tokens in the registry (project_url/files are plain references, never credentials).
135
+ auth: user
136
+
121
137
  # Hub platform + front-half review bridge (yad-connect-repos `detect-hub`; yad-review-gate + yad-hub-bridge).
122
138
  # The product hub is itself a git repo on a platform. With the bridge enabled, the front-half review/
123
139
  # comment/approval cycle runs through a real PR/MR on the hub: a review PR is opened per artifact, reviewers
@@ -11,7 +11,7 @@ set -euo pipefail
11
11
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
12
12
  cd "$ROOT"
13
13
 
14
- SKILLS=(yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-connect-repos yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-ship yad-backfill yad-run yad-review-gate yad-status)
14
+ SKILLS=(yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-connect-repos yad-connect-design yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-ship yad-backfill yad-run yad-review-gate yad-status)
15
15
 
16
16
  echo "Installing sdlc module from $ROOT/skills ..."
17
17
 
@@ -5,6 +5,7 @@ SDLC Workflow,yad-architecture,Author Architecture,AA,"Front state 3: with the a
5
5
  SDLC Workflow,yad-ui,Author UI Design,AU,"Front state 5: with the ux-designer author ui-design.md and DESIGN.md, driving Impeccable slash-commands when installed. Never auto-advances.",,{epic: EP-<slug>},1-front,yad-review-gate,yad-review-gate,true,epics/EP-<slug>/,ui-design.md DESIGN.md state.json
6
6
  SDLC Workflow,yad-stories,Author Stories,AS,"Front state 7: with the pm break the epic into repo-tagged stories with stable EP-<slug>-S0N IDs, one file each under stories/. Never auto-advances.",,{epic: EP-<slug>},1-front,yad-review-gate,yad-review-gate,true,epics/EP-<slug>/stories/,stories/EP-<slug>-S0N.md state.json
7
7
  SDLC Workflow,yad-connect-repos,Connect Code Repos,CR,"Setup/maintenance: connect code repos to the product hub so the front/brain phases are code-aware. Registers each repo (GitHub or GitLab, local-user auth, no stored tokens) in .sdlc/repos.json and caches a Repomix pack + a lightweight code-map (existing endpoints/events/data-models/modules, secret-scanned). Idempotent and refreshable; staleness tracked by HEAD sha. Run at setup or any time a new repo is added. Not a gated state β€” never touches epic state or approvals.",,{action: connect|refresh|list|disconnect} {repo: <name>} {path: <path-or-absolute>} {git_url: <ssh-or-https>} {domain_owner: <who>},0-setup,,yad-epic,false,.sdlc/,repos.json code-context/<repo>/pack.md code-context/<repo>/code-map.md
8
+ SDLC Workflow,yad-connect-design,Connect Design Tool,CD,"Setup/maintenance: connect a design tool (Figma-first, pluggable) to the product hub so the UI design step can materialize the actual feature design (mobile screens / web pages) inside it, alongside ui-design.md + DESIGN.md. Records the tool + project/file references in .sdlc/design.json (local-user / MCP-session auth, no stored tokens), detecting whether a design-tool MCP is available and degrading to markdown-only when absent. Idempotent and refreshable; one connection per project. Not a gated state β€” never touches epic state or approvals.",,{action: connect|refresh|list|disconnect} {tool: figma|pencil|none} {project_url: <team/project/file url>} {files: {web,mobile}},0-setup,,yad-ui,false,.sdlc/,design.json
8
9
  SDLC Workflow,yad-spec,Author Spec,SP,"Build-half Step A: for one ready-for-build story and one of its repos, run the heavy Spec Kit ceremony once (specifyβ†’clarifyβ†’planβ†’analyzeβ†’checklistβ†’tasks) inside that code repo, writing specs/<story-id>/ in Spec Kit's layout (drives /speckit.* when installed, else hand-authors and records speckit: not-installed). References the locked contract; never re-invents the surface. Writes link.md back to the story. Never auto-advances.",,{epic: EP-<slug>} {story: EP-<slug>-S0N} {repo: <one of story.repos>},3-build,yad-review-gate,,false,demo-repos/<repo>/specs/<story-id>/,spec.md research.md data-model.md contracts/ plan.md tasks.md link.md
9
10
  SDLC Workflow,yad-implement,Implement Task,IM,"Build-half Step B: with the dev lens, implement ONE atomic task from a story's tasks.md as a small diff (<=3 files) on its own branch feat/<story>-<task>-<slug> in the code repo. Diff stays inside the task's declared files (flag and STOP if it grows beyond them). Commit ends with a Task: <story>-<task> trailer; add Contract-Change: yes only when the locked contract surface is touched (routes back to the architecture gate). Never auto-advances.",,{epic: EP-<slug>} {story: EP-<slug>-S0N} {repo: <one of story.repos>} {task: T0N},3-build,yad-spec,,false,demo-repos/<repo>/,branch+commit per atomic task
10
11
  SDLC Workflow,yad-checks,Check Gates,CK,"Build-half Step C: wire and run the three production-safety CI gates on a code repo β€” spec-link (every change links a real story/spec via its Task trailer), contract-check (a contract-surface change without Contract-Change + an updated re-locked contract FAILS and routes back to the architecture gate), and build/test/lint. CI-agnostic bash invoked by GitHub Actions and GitLab CI. Blocking in CI; the human still owns the merge. Never auto-advances.",,{repo: <one of an epic's repos>} {action: wire|run} {base: target branch},3-build,yad-implement,,false,demo-repos/<repo>/,checks/*.sh .github/workflows/yad-checks.yml .gitlab-ci.yml
@@ -0,0 +1,117 @@
1
+ ---
2
+ name: yad-connect-design
3
+ description: 'Connects a design tool (Figma, or another tool β€” pluggable) to the product hub so the UI design step can materialize the full feature design (mobile screens / web pages) inside it, not just Markdown. Registers the tool into the project-wide .sdlc/design.json (local-user / MCP-session auth, no stored tokens), detecting whether a design-tool MCP is available and degrading to markdown-only when it is not. Run at setup or any time the design tool changes. Reusable, idempotent, refreshable. Use when the user says "connect Figma", "connect a design tool", "refresh the design connection", or "list the design connection".'
4
+ ---
5
+
6
+ # SDLC β€” Connect a Design Tool (make the UI step design-tool aware)
7
+
8
+ **Goal:** Let the UI design step (`yad-ui`) produce the **actual feature design** β€” the mobile screens
9
+ and/or web pages β€” inside a design tool such as **Figma**, alongside the Markdown artifacts
10
+ (`ui-design.md` / `DESIGN.md`). This skill **connects** a design tool to the product hub and records
11
+ *how* to reach it (the tool, the project/file references, which MCP renders it) β€” never a credential.
12
+
13
+ This is **setup/maintenance**, not a gated front state β€” it never touches `.sdlc/state.json` or any
14
+ epic's approvals. It only writes the project-wide design registry. `yad-ui` consumes it: when a tool is
15
+ connected and its MCP is available, the `ux-designer` lens **generates** screens into the tool (or
16
+ **links** an existing human-made design and reads it back); when nothing is connected, `yad-ui` runs
17
+ markdown-only exactly as before.
18
+
19
+ ## Conventions
20
+
21
+ - `{project-root}` resolves from the project working directory (the **product hub**).
22
+ - The integration is **Figma-first but pluggable** (`config.yaml` `design.tools`): a design-tool
23
+ *adapter*, like the `github`/`gitlab` platform adapter. Figma is the primary provider; `pencil`
24
+ (the `.pen` web/mobile editor) is a second, write-capable provider; `none` β†’ markdown-only.
25
+ - **The design tool is reached through its MCP** (a harness MCP server), NOT a subprocess CLI β€” the same
26
+ shape as Impeccable's slash-commands, not Repomix's `npx`. The skill detects the MCP and degrades when
27
+ it is absent; it never installs an MCP server.
28
+ - Registry: `{project-root}/.sdlc/design.json` (project-wide, shared across all epics β€” NOT per-epic),
29
+ the sibling of `.sdlc/repos.json` and `.sdlc/hub.json`.
30
+ - Per-epic screen→frame links are written later by `yad-ui` (`epics/EP-<slug>/.sdlc/design-links.json`),
31
+ not here.
32
+ - Speak in the configured `communication_language`; write documents in `document_output_language`.
33
+
34
+ ## Inputs
35
+
36
+ - `action` β€” `connect` (default) | `refresh` | `list` | `disconnect`.
37
+ - `tool` β€” `figma` | `pencil` | another adapter id (`config.yaml` `design.tools`). `none` records a
38
+ deliberate markdown-only project.
39
+ - `project_url` β€” the design tool's team/project/file reference (e.g. a Figma project or file URL).
40
+ Optional β€” a connection with no file yet is valid; `yad-ui` can create one on first generate.
41
+ - `files` β€” optional default file mapping per platform (`{ web: <ref>, mobile: <ref> }`).
42
+
43
+ ## On Activation
44
+
45
+ ### Step 1 β€” Resolve the tool and its MCP (the design-tool adapter)
46
+ Determine which tool is being connected from `tool` (default `figma`); reject a `tool` not in
47
+ `config.yaml` `design.tools` (fall back to the configured `design.primary` with a warning, the same way
48
+ `registerRepo` falls back on an unknown platform). Then **detect the tool's MCP** in this harness:
49
+
50
+ - **figma** → a Figma MCP server (Dev Mode MCP for read/link; html.to.design for HTML→Figma *generate*).
51
+ - **pencil** β†’ the `pencil` MCP (`batch_design` writes `.pen` web/mobile screens β€” generate-capable).
52
+ - another adapter β†’ its named MCP.
53
+
54
+ Record `provider` (the concrete MCP, e.g. `figma-mcp` | `html-to-design` | `pencil-mcp`) and whether it
55
+ is available. **Auth is the local user's own** β€” the user's authenticated MCP session. The skill
56
+ **stores no tokens**; `project_url`/`files` are plain references, never credentials.
57
+
58
+ **Graceful degradation:** if no design-tool MCP is available, record `source: "unavailable"` and report
59
+ that `yad-ui` will run **markdown-only** until an MCP is connected (no error β€” the design tool is purely
60
+ additive, exactly like Impeccable being absent). Do **not** install an MCP server as part of this step.
61
+
62
+ ### Step 2 β€” Record the connection in the registry
63
+ Upsert into `{project-root}/.sdlc/design.json` (create the file + parent `.sdlc/` if absent):
64
+
65
+ ```json
66
+ {
67
+ "tool": "figma",
68
+ "provider": "figma-mcp",
69
+ "project_url": "https://www.figma.com/files/project/<id>/<name>",
70
+ "auth": "user",
71
+ "files": { "web": null, "mobile": null },
72
+ "connectedAt": "<YYYY-MM-DD>",
73
+ "lastSyncedAt": "<YYYY-MM-DD>",
74
+ "source": "figma-mcp"
75
+ }
76
+ ```
77
+
78
+ - `tool: "none"` records a deliberate markdown-only project: `{ "tool": "none", "provider": null,
79
+ "source": "unavailable", ... }`.
80
+ - `connect` is **idempotent** β€” re-running it overwrites the single connection in place (a project has
81
+ one design tool at a time; switching tools is just another `connect`).
82
+
83
+ ### Step 3 β€” Report (never auto-advance)
84
+ Report the connected `tool`, its `provider`, whether the MCP is available (or that `yad-ui` will degrade
85
+ to markdown-only), the `project_url`, and that **`yad-ui` will now generate/link the design here**.
86
+ Nothing auto-advances; this is setup.
87
+
88
+ ## Other actions
89
+
90
+ - **`refresh`** β€” re-detect the MCP and update `lastSyncedAt` (after the user authenticates a session or
91
+ changes tools). Same machinery as `connect`. Re-detection may flip `source` between an MCP id and
92
+ `unavailable` β€” report the change.
93
+ - **`list`** β€” print the current connection: `tool`, `provider`, `project_url`, the file mapping, and a
94
+ **available/unavailable** flag for the MCP (best-effort, the user's own session). No design tool
95
+ connected β‡’ "markdown-only".
96
+ - **`disconnect`** β€” remove the registry file (or set `tool: "none"`). The design tool's own
97
+ project/files are **never touched** β€” only the hub's record of them.
98
+
99
+ ## Hard rules
100
+
101
+ - **Local-user / MCP-session auth only; store no tokens.** Connect through the user's authenticated MCP
102
+ session; never embed a Figma PAT or any credential in the registry. `project_url`/`files` are plain
103
+ references.
104
+ - **Degrade gracefully.** No design tool / no MCP β†’ `yad-ui` runs markdown-only with no error. The design
105
+ tool is additive, never a blocker β€” the same discipline as Impeccable and the `gh`/`glab` bridge.
106
+ - **Setup, not a gate.** Never touch `.sdlc/state.json`, approvals, or the contract lock from here.
107
+ - **Idempotent + refreshable.** `connect`/`refresh` are safe to re-run; a project carries one design
108
+ connection at a time.
109
+ - **Describe the connection; do not design here.** This skill records *how to reach* the tool. The actual
110
+ screens are generated/linked by `yad-ui`, per epic.
111
+
112
+ ## Reference
113
+ - Registry schema + freshness rule: `references/design-registry.md`.
114
+ - MCP detection per provider, the generate-vs-link recipes, the degrade path, and the honest
115
+ write-vs-read-only MCP capability note: `references/design-context.md`.
116
+ - The connect pattern this mirrors (code repos): `../yad-connect-repos/SKILL.md`.
117
+ - The consumer β€” how `yad-ui` generates/links and writes `design-links.json`: `../yad-ui/SKILL.md`.
@@ -0,0 +1,64 @@
1
+ # Design context β€” MCP detection, generate vs link, and the degrade path
2
+
3
+ How a connected design tool turns into the actual feature design the UI step materializes. The design
4
+ tool is reached through its **MCP** (a harness MCP server), the same shape as Impeccable's
5
+ slash-commands β€” detect it, use it when present, degrade cleanly when absent. This is the design-side
6
+ analogue of `yad-connect-repos`'s code-context.
7
+
8
+ ## Provider detection
9
+
10
+ `connect`/`refresh` records `provider` (the concrete MCP) and `source` (the MCP id, or `unavailable`).
11
+ Detection is best-effort against the user's own authenticated MCP session:
12
+
13
+ | `tool` | MCP / provider | Capability |
14
+ |--------|----------------|------------|
15
+ | `figma` | a Figma Dev Mode MCP | **read/link** β€” reference a file, read frames back into `ui-design.md` |
16
+ | `figma` | html.to.design MCP (`import-html`) | **generate** β€” render HTML/CSS screens into a Figma file |
17
+ | `pencil` | the `pencil` MCP (`batch_design`) | **generate** β€” author `.pen` web/mobile screens directly |
18
+ | other | the adapter's named MCP | per that adapter |
19
+
20
+ **Honest capability note:** not every design-tool MCP can *write*. A read-only Figma Dev Mode MCP
21
+ supports **link + read-back** only; *generate* needs a write-capable provider (html.to.design for Figma,
22
+ or `pencil`). `yad-ui` picks the direction the connected provider actually supports and records which one
23
+ it used (`direction: generated | linked`). It never claims to have generated screens a read-only MCP
24
+ cannot produce.
25
+
26
+ ## Generate (push screens into the tool)
27
+
28
+ When the connected provider is write-capable, the `ux-designer` lens produces the epic's screens in the
29
+ tool, covering the screens/states `ui-design.md` enumerates and the user flows the architecture defines:
30
+
31
+ - **Figma via html.to.design** β€” the lens drafts each screen as HTML/CSS (reusing `DESIGN.md` tokens),
32
+ then imports it into the Figma file via the MCP's `import-html`, one frame per screen.
33
+ - **pencil** β€” the lens calls `batch_design` to author the screens as `.pen` frames directly (mobile
34
+ and/or web per the epic).
35
+
36
+ Reuse what already exists: load the connected code repos' code-maps (`yad-ui` Step 2b) and any
37
+ Impeccable `DESIGN.md` tokens so generated screens match built components, not invented ones.
38
+
39
+ ## Link (reference a human-made design)
40
+
41
+ When a designer has already built the screens (or the provider is read-only), point `yad-ui` at the
42
+ existing file and **read the frames back** so `ui-design.md` reflects the real design: list each frame as
43
+ a screen, capture its node id + URL, and map components/tokens into `DESIGN.md`.
44
+
45
+ ## Write back the linkage (done by `yad-ui`, per epic)
46
+
47
+ Either direction ends by writing `epics/EP-<slug>/.sdlc/design-links.json` β€” the machine-readable
48
+ screen→frame map — and a `## Design (<tool>)` section in `ui-design.md` linking each screen to its frame
49
+ URL. The design itself lives in the tool; the hub keeps the *links* and the Markdown spec beside the
50
+ other epic artifacts.
51
+
52
+ ## Degrade path (no MCP / no tool)
53
+
54
+ If `design.json` is absent, `tool: "none"`, or `source: "unavailable"`, `yad-ui` runs **markdown-only**:
55
+ it authors `ui-design.md` / `DESIGN.md` exactly as before and records `design: none` in the frontmatter
56
+ with a one-line note (mirroring the `impeccable: not-installed` degrade). No error β€” the design tool is
57
+ purely additive.
58
+
59
+ ## Staleness / refresh
60
+
61
+ A re-generated or designer-edited file is like a moved code repo: `yad-ui` **flags** a divergence and
62
+ lets a human decide (re-run the step, or `yad-connect-design` action: refresh). It never silently
63
+ overwrites a designer's frames β€” refreshing the design is a human decision, the same discipline as
64
+ `code_context.refresh: human`.
@@ -0,0 +1,54 @@
1
+ # Design registry β€” schema + freshness rule
2
+
3
+ The registry is the product hub's record of which design tool is connected and how to reach it. It is
4
+ **project-wide** (one design tool per project, shared across every epic), so it lives at the product
5
+ root, not under any `epics/EP-<slug>/.sdlc/`.
6
+
7
+ ## Location
8
+
9
+ `{project-root}/.sdlc/design.json`
10
+
11
+ (`config.yaml` `design.registry`.) Create the file and its parent `.sdlc/` on the first `connect`.
12
+
13
+ ## Schema
14
+
15
+ ```json
16
+ {
17
+ "tool": "figma", // figma | pencil | <adapter id> | none (markdown-only)
18
+ "provider": "figma-mcp", // the concrete MCP: figma-mcp | html-to-design | pencil-mcp | null
19
+ "project_url": "https://www.figma.com/files/project/123/feature", // team/project/file reference; null if none yet
20
+ "auth": "user", // ALWAYS the user's own MCP session β€” never a token
21
+ "files": { "web": null, "mobile": null }, // optional default file refs per platform
22
+ "connectedAt": "2026-06-13", // first connect (YYYY-MM-DD)
23
+ "lastSyncedAt": "2026-06-13", // last connect/refresh
24
+ "source": "figma-mcp" // the MCP detected at connect | unavailable (degraded)
25
+ }
26
+ ```
27
+
28
+ ## Rules
29
+
30
+ - **`tool`** selects the adapter; it MUST be one of `config.yaml` `design.tools` (or `none`). An unknown
31
+ tool falls back to `design.primary` with a warning β€” never silently accepted.
32
+ - **Auth is never stored.** No Figma PAT, OAuth token, or any credential in the registry. `project_url`
33
+ and `files` are plain references; `connect` reaches the tool through the user's authenticated MCP
34
+ session.
35
+ - **`connect` overwrites in place** β€” a project carries exactly one design connection at a time;
36
+ switching tools is just another `connect`. There is no array (unlike `repos.json`).
37
+ - **`source`** is the authority for availability: an MCP id (`figma-mcp` / `pencil-mcp` / …) means
38
+ `yad-ui` can generate/link; `unavailable` means `yad-ui` degrades to markdown-only. `refresh`
39
+ re-detects and may flip it.
40
+ - **`tool: "none"`** is a valid, deliberate state: a project that has chosen markdown-only. `yad-ui`
41
+ treats it exactly like an absent registry.
42
+ - **`disconnect`** removes the file (or sets `tool: "none"`). The design tool's own project/files are
43
+ never touched.
44
+
45
+ ## Git tracking
46
+
47
+ Commit the **registry** (`design.json`) β€” it is small, reviewable, and holds no secrets (references
48
+ only). This mirrors how `repos.json` and `hub.json` are committed.
49
+
50
+ ## Greenfield
51
+
52
+ A brand-new product hub has no `design.json`. That is valid β€” `yad-ui` treats "no design tool connected"
53
+ the same as `tool: "none"` and produces the Markdown artifacts only. The registry appears the first time
54
+ `connect` runs.
@@ -86,6 +86,19 @@ PR/MR opened on the hub (sibling of `approvals.json`, so the locked `state.json`
86
86
  { "step": "<review step id>", "artifact": "<artifact>", "platform": "github|gitlab", "number": <n>, "url": "<pr/mr url>", "branch": "review/EP-<slug>/<artifact-base>", "lastSyncedAt": "<YYYY-MM-DD or null>" }
87
87
  ```
88
88
 
89
+ ## `design-links.json`
90
+ Present only when the `ui-design` step materialized the design in a connected design tool
91
+ (`yad-connect-design` → `.sdlc/design.json`). Written by `yad-ui`, the machine-readable screen→frame map
92
+ (sibling of `contract-lock.json`; the locked `state.json` step shape is untouched). The `ui-design` step
93
+ chain is unchanged β€” this is an *output enrichment*, mirrored by the `design:` frontmatter block and the
94
+ `## Design (<tool>)` section in `ui-design.md`. Absent when the step ran markdown-only (`design: none`).
95
+
96
+ ```json
97
+ { "tool": "figma", "fileUrl": "<url>", "generatedAt": "<YYYY-MM-DD>", "direction": "generated|linked",
98
+ "screens": [ { "name": "<screen>", "platform": "mobile|web", "nodeId": "<id>", "url": "<frame url>" } ],
99
+ "source": "<mcp id>" }
100
+ ```
101
+
89
102
  ## `reviews/`
90
103
  Human-readable review records, one file per round:
91
104
  `reviews/<artifact-base>--<YYYY-MM-DD>--<status>.md` where `status` ∈ `comments` | `approved`
@@ -6,18 +6,28 @@ description: 'Front state 5 of the gated SDLC. With the ux-designer, author ui-d
6
6
  # SDLC β€” Author UI Design (front state 5)
7
7
 
8
8
  **Goal:** Produce a human-authored, AI-assisted `ui-design.md` and `DESIGN.md` for an approved
9
- architecture. This is a **front state**: human-authored with AI assist, **never auto-advances**. When
10
- the UI is drafted, control passes to `yad-review-gate` (base rule: owner + 1 reviewer).
9
+ architecture **and**, when a design tool is connected, the **actual feature design** β€” the mobile
10
+ screens and/or web pages β€” inside that tool (e.g. Figma), linked back from the artifacts. This is a
11
+ **front state**: human-authored with AI assist, **never auto-advances**. When the UI is drafted, control
12
+ passes to `yad-review-gate` (base rule: owner + 1 reviewer).
11
13
 
12
14
  UI work is shaped by **Impeccable**, invoked as **harness slash-commands** (not a subprocess CLI) per
13
15
  the Phase 0 deviation. If Impeccable is not installed, the `ux-designer` lens authors the same outputs
14
16
  directly β€” the workflow does not block on the tool.
15
17
 
18
+ The visual design is materialized in the **design tool connected via `yad-connect-design`**
19
+ (`.sdlc/design.json`), reached through its MCP. When a tool is connected the `ux-designer` lens
20
+ **generates** screens into it (or **links** an existing human-made design and reads it back); when none
21
+ is connected, the step degrades to the Markdown artifacts only β€” the design tool is additive, exactly
22
+ like Impeccable.
23
+
16
24
  ## Conventions
17
25
 
18
26
  - `{project-root}` resolves from the project working directory.
19
27
  - Artifacts live under `{project-root}/epics/EP-<slug>/` (build plan Β§6).
20
28
  - `DESIGN.md` is Impeccable's conventional root design-system file (RESEARCH-NOTES Β§4).
29
+ - The connected design tool is recorded in `{project-root}/.sdlc/design.json` (`config.yaml` `design`),
30
+ written by `yad-connect-design`. The per-epic screen→frame map is `design-links.json` (Step 4b).
21
31
  - Speak in the configured `communication_language`; write documents in `document_output_language`.
22
32
 
23
33
  ## On Activation
@@ -66,6 +76,25 @@ components/tokens into the design system; `/impeccable craft` is shape-then-buil
66
76
  that Impeccable was not used**. Do not run `npx impeccable skills install` as part of this step β€” tool
67
77
  installation is out of scope for the front half.
68
78
 
79
+ ### Step 3b β€” Materialize the design in the connected tool (generate or link)
80
+ Read `{project-root}/.sdlc/design.json` (`config.yaml` `design.registry`). Decide the path:
81
+
82
+ - **No tool / `tool: "none"` / `source: "unavailable"`** (or the file is absent): **degrade** β€”
83
+ author the Markdown artifacts only and record `design: none` in the frontmatter with a one-line note
84
+ (mirrors the `impeccable: not-installed` degrade). Skip to Step 4.
85
+ - **A tool is connected and its MCP is available:** adopt the `ux-designer` lens and, using the provider
86
+ recorded in `design.json` (Figma via a Figma/html.to.design MCP, `pencil` via its MCP, etc.):
87
+ - **Generate** β€” when the provider is write-capable, produce one frame per screen the design covers,
88
+ for the platforms in `epic.repos` (mobile and/or web), reusing the code-maps (Step 2b) and
89
+ `DESIGN.md` tokens so screens match built components rather than inventing parallel ones.
90
+ - **Link** β€” when the user points at an existing design file (or the provider is read-only), reference
91
+ it and **read the frames back** so `ui-design.md` reflects the real design.
92
+
93
+ Capture each screen's `name`, `platform`, `nodeId`, and `url`. Record which direction was used
94
+ (`generated | linked`). Honest-capability rule: a read-only MCP supports **link** only β€” never claim a
95
+ screen was generated that the provider cannot produce. See
96
+ `../yad-connect-design/references/design-context.md`.
97
+
69
98
  ### Step 4 β€” Write the UI artifacts
70
99
  Write `{project-root}/epics/EP-<slug>/ui-design.md` using EXACTLY this template:
71
100
 
@@ -78,6 +107,7 @@ owner: <inherit from epic.md owner> # the epic owner carries through; not rety
78
107
  repos: [<inherit from epic>]
79
108
  impeccable: <used | not-installed>
80
109
  code-context: { repos: [], loaded: <YYYY-MM-DD or none> } # code-maps that informed component reuse (Step 2b)
110
+ design: <none | { tool: <figma|pencil|…>, direction: <generated|linked>, file: <url>, screens: <N> }> # the connected design tool (Step 3b)
81
111
  ---
82
112
 
83
113
  ## Screens & states
@@ -91,23 +121,52 @@ code-context: { repos: [], loaded: <YYYY-MM-DD or none> } # code-maps that inf
91
121
 
92
122
  ## Accessibility & responsiveness
93
123
  <!-- a11y notes; breakpoints/viewports covered -->
124
+
125
+ ## Design (<tool>)
126
+ <!-- omit this section when design: none. one row per screen, linking to its frame in the tool.
127
+ mirrors design-links.json (Step 4b). -->
128
+ <!-- - <Screen name> (<mobile|web>) β€” <frame url> -->
94
129
  ```
95
130
 
96
131
  Also create/update `{project-root}/epics/EP-<slug>/DESIGN.md` (Impeccable's design-system file, or a
97
132
  hand-authored equivalent when degraded) capturing the design tokens/components the screens rely on.
98
133
 
134
+ ### Step 4b β€” Write the design-links map (when a tool was used)
135
+ When Step 3b generated or linked a design, write the machine-readable screen→frame map to
136
+ `{project-root}/epics/EP-<slug>/.sdlc/design-links.json` (sibling of `contract-lock.json`):
137
+
138
+ ```json
139
+ {
140
+ "tool": "figma",
141
+ "fileUrl": "https://www.figma.com/file/<key>/<name>",
142
+ "generatedAt": "<YYYY-MM-DD>",
143
+ "direction": "generated | linked",
144
+ "screens": [
145
+ { "name": "Submit Inquiry", "platform": "mobile",
146
+ "nodeId": "123:45", "url": "https://www.figma.com/file/<key>/?node-id=123-45" }
147
+ ],
148
+ "source": "figma-mcp"
149
+ }
150
+ ```
151
+
152
+ Keep the `## Design (<tool>)` section of `ui-design.md` in step with this file. When Step 3b degraded
153
+ (`design: none`), do **not** write `design-links.json`.
154
+
99
155
  ### Step 5 β€” Advance the authoring step (NOT the gate)
100
156
  In `state.json`: set `ui-design.status: "done"`, set `ui-design-review.status: "in_review"`, and set
101
157
  `currentStep: "ui-design-review"`. Write `state.json`. Do **not** touch `approvals.json`.
102
158
 
103
159
  ### Step 6 β€” Stop at the gate (do NOT advance)
104
- Report: the paths to `ui-design.md` and `DESIGN.md`, whether Impeccable was used, and that the next
105
- action is **review** via `yad-review-gate` (base rule: owner + 1 reviewer). **Never record approval
106
- here.** Front states do not auto-advance. When the hub has a platform, the gate opens a review PR on the
160
+ Report: the paths to `ui-design.md` and `DESIGN.md`, whether Impeccable was used, the connected design
161
+ tool and what it produced (e.g. "Figma β€” 4 screens generated", the file URL + `design-links.json` path,
162
+ or "no design tool β€” markdown-only"), and that the next action is **review** via `yad-review-gate` (base
163
+ rule: owner + 1 reviewer). **Never record approval here.** Front states do not auto-advance. When the hub has a platform, the gate opens a review PR on the
107
164
  hub (via `yad-hub-bridge`) and `yad-review-gate action: sync` pulls platform approvals/comments into
108
165
  the ledger; otherwise the review is recorded file-only.
109
166
 
110
167
  ## Reference
111
168
  - Impeccable commands and the slash-command-vs-CLI deviation: `RESEARCH-NOTES.md` Β§4 + Deviation 3.
112
169
  - State schema and field meanings: `../yad-epic/references/state-schema.md`.
170
+ - Connecting a design tool (Figma, pluggable) + generate-vs-link recipes and the degrade path:
171
+ `../yad-connect-design/SKILL.md` (+ `references/design-context.md`).
113
172
  - Connecting code repos + the code-context the brain reads: `../yad-connect-repos/SKILL.md`.