yadflow 2.12.0 → 2.14.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/cli/setup.mjs CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import {
6
- c, log, step, ok, info, warn, hand, fail, ask, askYesNo, run, has,
6
+ c, log, step, guide, ok, info, warn, hand, fail, ask, askYesNo, run, has,
7
7
  exists, readJSON, writeJSON,
8
8
  } from './lib.mjs';
9
9
  import { VERSION, IDE_FOLDER_TARGETS, PROJECT_FILES, DESIGN_TOOLS, DESIGN_PRIMARY, TESTING_TOOLS, TESTING_PRIMARY, LEARNING_TOOLS, LEARNING_PRIMARY } from './manifest.mjs';
@@ -176,7 +176,7 @@ export function reconcileRepoRoles(root, name, repo, current = [], want = []) {
176
176
  ok(` ${repo}: ${want.length ? want.join(', ') : 'cleared'}`);
177
177
  }
178
178
 
179
- export function registerRepo(root, registry, { name, rpath, platform, domain_owner = '', domain_owners = null, default_branch = 'main', today = null }) {
179
+ export function registerRepo(root, registry, { name, rpath, platform, domain_owner = '', domain_owners = null, default_branch = 'main', today = null, pack = true }) {
180
180
  if (!insideRoot(root, rpath)) {
181
181
  warn(`${rpath} resolves outside the project root — skipped`);
182
182
  return null;
@@ -198,7 +198,10 @@ export function registerRepo(root, registry, { name, rpath, platform, domain_own
198
198
  name, path: rpath, git_url: (remote.ok && remote.stdout) || null, platform: plat,
199
199
  domain_owner: owners[0] || '', domain_owners: owners, default_branch,
200
200
  connectedAt: today, lastSyncedAt: today,
201
- syncedHead: head,
201
+ // Only claim a synced HEAD when a pack is actually produced. The greenfield path skips packing
202
+ // (pack:false), so leave syncedHead null — the repo then reads as "needs an initial pack" in
203
+ // `yad repo list` / `yad doctor` instead of falsely "fresh".
204
+ syncedHead: pack ? head : null,
202
205
  contextPack: `.sdlc/code-context/${name}/pack.md`,
203
206
  codeMap: `.sdlc/code-context/${name}/code-map.md`,
204
207
  source: 'repomix',
@@ -305,13 +308,70 @@ function applyActions(actions, { force = false } = {}) {
305
308
  return changed;
306
309
  }
307
310
 
311
+ // Step 0 — resolve the setup profile that branches the rest of the wizard. Flags pre-answer each
312
+ // question (CI/scripts); an existing hub.json carries prior answers forward (idempotent re-run);
313
+ // otherwise we prompt with a default. Pure of side effects — it only reads. Returns
314
+ // { solo, team_size, codebase, repo_layout, configureTools }.
315
+ export async function resolveProfile(root, opts = {}) {
316
+ const hub = readJSON(path.join(root, PROJECT_FILES.hubConfig), null);
317
+ const prev = (hub && hub.profile) || {};
318
+
319
+ // 1. Solo or team (+ size). --solo / --team <n> win; else carry hub.solo forward; else ask.
320
+ let solo, team_size;
321
+ if (opts.solo) { solo = true; team_size = 1; }
322
+ else if (opts.team != null) { team_size = Math.max(1, parseInt(opts.team, 10) || 1); solo = team_size <= 1; }
323
+ else if (typeof hub?.solo === 'boolean') { solo = hub.solo; team_size = prev.team_size ?? (solo ? 1 : 2); }
324
+ else {
325
+ // Default from any existing roster: a hub already carrying reviewers is a team; otherwise solo.
326
+ const rosterN = Array.isArray(hub?.roster) ? hub.roster.length : 0;
327
+ solo = !(await ask('Solo or team?', rosterN > 1 ? 'team' : 'solo')).toLowerCase().startsWith('t');
328
+ team_size = solo ? 1 : Math.max(2, parseInt(await ask(' how many team members?', String(rosterN || 2)), 10) || 2);
329
+ }
330
+
331
+ // 2. Greenfield (new code) or brownfield (existing code).
332
+ let codebase;
333
+ if (opts.greenfield) codebase = 'greenfield';
334
+ else if (opts.brownfield) codebase = 'brownfield';
335
+ else if (prev.codebase) codebase = prev.codebase;
336
+ else codebase = (await ask('Greenfield (new code) or brownfield (existing code)?', 'greenfield')).toLowerCase().startsWith('b') ? 'brownfield' : 'greenfield';
337
+
338
+ // 3. Monorepo (one repo) or separate repos.
339
+ let repo_layout;
340
+ if (opts.monorepo) repo_layout = 'monorepo';
341
+ else if (opts.separate) repo_layout = 'separate';
342
+ else if (prev.repo_layout) repo_layout = prev.repo_layout;
343
+ else repo_layout = (await ask('Monorepo (one repo) or separate repos?', 'monorepo')).toLowerCase().startsWith('s') ? 'separate' : 'monorepo';
344
+
345
+ // 4. Configure the optional tools now, or defer (records them as none, connect later).
346
+ const configureTools = opts.tools === true ? true
347
+ : process.env.SDLC_NONINTERACTIVE ? false
348
+ : await askYesNo('Configure design/testing/learning tools now? (else connect them later)', false);
349
+
350
+ return { solo, team_size, codebase, repo_layout, configureTools };
351
+ }
352
+
353
+ // The guided, idempotent first-run wizard: a Step 0 profile interview (resolveProfile) that branches
354
+ // the remaining steps — install, hub + roster, optional tools, repos, wiring — and persists the profile.
308
355
  export async function runSetup(root, opts = {}) {
309
- const total = 10;
310
356
  log(c.bold(`\nSDLC Workflow setup ${c.dim('v' + VERSION)}`));
311
357
  log(c.dim(`target: ${root}`));
312
358
 
313
- // 1. Preflight
314
- step(1, total, 'Preflight');
359
+ // 0. Profile interview — branch the wizard to the user's situation (solo/team, code, repo layout).
360
+ const { solo, team_size, codebase, repo_layout, configureTools } = await resolveProfile(root, opts);
361
+ // Steps: interview, preflight, install, hub, tools (1 if deferred else 3), repos, wire, coderabbit, done.
362
+ const total = 8 + (configureTools ? 3 : 1);
363
+ let _n = 0;
364
+ const S = (title) => step(++_n, total, title);
365
+
366
+ S('Profile');
367
+ guide([
368
+ 'How you answer here shapes the rest of setup — fewer prompts, the right path.',
369
+ `solo: ${solo ? 'yes — you review by merging your own PR (approval waived)' : `no — team of ${team_size}`}`,
370
+ `code: ${codebase} • repos: ${repo_layout} • optional tools: ${configureTools ? 'configure now' : 'deferred (connect later)'}`,
371
+ ]);
372
+
373
+ // Preflight
374
+ S('Preflight');
315
375
  if (!exists(path.join(root, '.git'))) {
316
376
  if (await askYesNo('Not a git repo. Run `git init` here?', true)) {
317
377
  run('git', ['init'], { cwd: root });
@@ -321,8 +381,12 @@ export async function runSetup(root, opts = {}) {
321
381
  for (const tool of ['git', 'node']) has(tool) ? ok(`${tool} present`) : warn(`${tool} not found on PATH`);
322
382
  if (!has('npx')) warn('npx not found — repomix packing will be skipped');
323
383
 
324
- // 2. Install the module
325
- step(2, total, 'Install the module (skills + _bmad registration)');
384
+ // Install the module
385
+ S('Install the module (skills + _bmad registration)');
386
+ guide([
387
+ 'Copies the yad-* skills into your AI tool(s) so they appear in Claude Code / agents / opencode.',
388
+ 'Enter the IDE folders to install into, comma-separated; default = whatever is already present.',
389
+ ]);
326
390
  let ideTargets = opts.ideTargets;
327
391
  if (!ideTargets) {
328
392
  const present = ALL_IDES.filter((d) => exists(path.join(root, d)));
@@ -352,8 +416,18 @@ export async function runSetup(root, opts = {}) {
352
416
  }
353
417
  }
354
418
 
355
- // 3. Detect hub platform + roster
356
- step(3, total, 'Hub platform & reviewer roster');
419
+ // Detect hub platform + roster
420
+ S(solo ? 'Hub platform (solo — no roster)' : 'Hub platform & reviewer roster');
421
+ guide(solo
422
+ ? [
423
+ 'Your hub is this repo on GitHub/GitLab (or none for a file-only gate).',
424
+ 'Solo: no roster needed — you review by merging your own PR (approval waived).',
425
+ ]
426
+ : [
427
+ 'Your hub is this repo on GitHub/GitLab; reviewers approve artifacts there.',
428
+ `Add your ${team_size}-person roster: platform login → yad name → hub role (owner/reviewer).`,
429
+ 'An owner + 1 reviewer is required to pass a gate; skip now and add later with `yad roster add`.',
430
+ ]);
357
431
  const hubPath = path.join(root, PROJECT_FILES.hubConfig);
358
432
  if (exists(hubPath) && !(await askYesNo('hub.json exists — reconfigure?', false))) {
359
433
  info('keeping existing .sdlc/hub.json');
@@ -367,7 +441,8 @@ export async function runSetup(root, opts = {}) {
367
441
  platform = 'none';
368
442
  }
369
443
  const roster = [];
370
- if (await askYesNo('Add reviewers to the roster now?', true)) {
444
+ // Solo mode needs no roster — the lone developer is owner and reviewer-by-merge.
445
+ if (!solo && await askYesNo('Add reviewers to the roster now?', true)) {
371
446
  for (;;) {
372
447
  const login = await ask(' reviewer platform login (blank to finish)', '');
373
448
  if (!login) break;
@@ -392,68 +467,113 @@ export async function runSetup(root, opts = {}) {
392
467
  // `bridge_enabled` is the canonical flag (hub-config schema); keep the legacy `bridge` spelling
393
468
  // for anything that still reads it.
394
469
  const enabled = platform !== 'none';
395
- writeJSON(hubPath, { platform: enabled ? platform : null, bridge_enabled: enabled, bridge: enabled, default_branch, roster });
396
- ok(`wrote ${PROJECT_FILES.hubConfig} (${roster.length} reviewer(s))`);
470
+ writeJSON(hubPath, { platform: enabled ? platform : null, bridge_enabled: enabled, bridge: enabled, default_branch, roster, solo, profile: { codebase, repo_layout, team_size } });
471
+ ok(`wrote ${PROJECT_FILES.hubConfig} (${roster.length} reviewer(s)${solo ? ', solo mode' : ''})`);
397
472
  }
398
-
399
- // 4. Connect a design tool (Figma-first, pluggable; the UI step materializes the design here)
400
- step(4, total, 'Connect a design tool (Figma / pencil / none)');
401
- const designPath = path.join(root, PROJECT_FILES.designConfig);
402
- if (exists(designPath) && !(await askYesNo('design.json exists reconfigure?', false))) {
403
- info('keeping existing .sdlc/design.json');
404
- } else {
405
- let tool = (await ask(`Design tool (${DESIGN_TOOLS.join('/')}/none)`, DESIGN_PRIMARY)).toLowerCase();
406
- if (![...DESIGN_TOOLS, 'none'].includes(tool)) {
407
- warn(`unknown design tool '${tool}' — using ${DESIGN_PRIMARY}`);
408
- tool = DESIGN_PRIMARY;
473
+ // Persist the profile + solo flag even on the "keeping existing" path, so re-running setup with new
474
+ // flags (e.g. `yad setup --solo`) updates the mode without a full reconfigure. Merge, never clobber.
475
+ if (exists(hubPath)) {
476
+ const cur = readJSON(hubPath, {}) || {};
477
+ if (cur.solo !== solo || JSON.stringify(cur.profile || {}) !== JSON.stringify({ codebase, repo_layout, team_size })) {
478
+ writeJSON(hubPath, { ...cur, solo, profile: { codebase, repo_layout, team_size } });
479
+ info(`recorded profile: ${solo ? 'solo' : `team(${team_size})`}, ${codebase}, ${repo_layout}`);
409
480
  }
410
- const project_url = tool === 'none' ? null : (await ask(' project/file URL (blank to set later)', '')) || null;
411
- registerDesign(root, { tool, project_url, today: opts.today ?? null });
412
- ok(tool === 'none'
413
- ? `wrote ${PROJECT_FILES.designConfig} (markdown-only)`
414
- : `wrote ${PROJECT_FILES.designConfig} (${tool})`);
415
481
  }
416
482
 
417
- // 5. Connect a testing tool (Playwright-first, pluggable; the test-cases step implements automation here)
418
- step(5, total, 'Connect a testing tool (playwright / cypress / pytest / none)');
483
+ // Optional tools (design / testing / learning). Paths are declared here so the final summary can
484
+ // read them whether or not we configured the tools this run.
485
+ const designPath = path.join(root, PROJECT_FILES.designConfig);
419
486
  const testingPath = path.join(root, PROJECT_FILES.testingConfig);
420
- if (exists(testingPath) && !(await askYesNo('testing.json exists — reconfigure?', false))) {
421
- info('keeping existing .sdlc/testing.json');
422
- } else {
423
- let tool = (await ask(`Testing tool (${TESTING_TOOLS.join('/')}/none)`, TESTING_PRIMARY)).toLowerCase();
424
- if (![...TESTING_TOOLS, 'none'].includes(tool)) {
425
- warn(`unknown testing tool '${tool}' using ${TESTING_PRIMARY}`);
426
- tool = TESTING_PRIMARY;
487
+ const learningPath = path.join(root, PROJECT_FILES.learningConfig);
488
+ if (configureTools) {
489
+ // Connect a design tool (Figma-first, pluggable; the UI step materializes the design here)
490
+ S('Connect a design tool (Figma / pencil / none)');
491
+ guide([
492
+ 'Where yad-ui materializes real screens. figma (confirm the MCP later) or none for markdown-only.',
493
+ 'Skipping is safe — the UI step degrades to ui-design.md.',
494
+ ]);
495
+ if (exists(designPath) && !(await askYesNo('design.json exists — reconfigure?', false))) {
496
+ info('keeping existing .sdlc/design.json');
497
+ } else {
498
+ let tool = (await ask(`Design tool (${DESIGN_TOOLS.join('/')}/none)`, DESIGN_PRIMARY)).toLowerCase();
499
+ if (![...DESIGN_TOOLS, 'none'].includes(tool)) {
500
+ warn(`unknown design tool '${tool}' — using ${DESIGN_PRIMARY}`);
501
+ tool = DESIGN_PRIMARY;
502
+ }
503
+ const project_url = tool === 'none' ? null : (await ask(' project/file URL (blank to set later)', '')) || null;
504
+ registerDesign(root, { tool, project_url, today: opts.today ?? null });
505
+ ok(tool === 'none'
506
+ ? `wrote ${PROJECT_FILES.designConfig} (markdown-only)`
507
+ : `wrote ${PROJECT_FILES.designConfig} (${tool})`);
427
508
  }
428
- const project_url = tool === 'none' ? null : (await ask(' project/config reference (blank to set later)', '')) || null;
429
- registerTesting(root, { tool, project_url, today: opts.today ?? null });
430
- ok(tool === 'none'
431
- ? `wrote ${PROJECT_FILES.testingConfig} (artifacts-only)`
432
- : `wrote ${PROJECT_FILES.testingConfig} (${tool})`);
433
- }
434
509
 
435
- // 6. Connect a learning tool (DeepTutor-first, pluggable; the learning layer tutors team members here)
436
- step(6, total, 'Connect a learning tool (deeptutor / none)');
437
- const learningPath = path.join(root, PROJECT_FILES.learningConfig);
438
- if (exists(learningPath) && !(await askYesNo('learning.json exists reconfigure?', false))) {
439
- info('keeping existing .sdlc/learning.json');
440
- } else {
441
- let tool = (await ask(`Learning tool (${LEARNING_TOOLS.join('/')}/none)`, LEARNING_PRIMARY)).toLowerCase();
442
- if (![...LEARNING_TOOLS, 'none'].includes(tool)) {
443
- warn(`unknown learning tool '${tool}' using ${LEARNING_PRIMARY}`);
444
- tool = LEARNING_PRIMARY;
510
+ // Connect a testing tool (Playwright-first, pluggable; the test-cases step implements automation here)
511
+ S('Connect a testing tool (playwright / cypress / pytest / none)');
512
+ guide([
513
+ 'Where yad-test-cases generates automation. playwright/cypress/pytest, or none for artifacts-only.',
514
+ 'Skipping is safe — test-cases authors test-cases.md only.',
515
+ ]);
516
+ if (exists(testingPath) && !(await askYesNo('testing.json exists — reconfigure?', false))) {
517
+ info('keeping existing .sdlc/testing.json');
518
+ } else {
519
+ let tool = (await ask(`Testing tool (${TESTING_TOOLS.join('/')}/none)`, TESTING_PRIMARY)).toLowerCase();
520
+ if (![...TESTING_TOOLS, 'none'].includes(tool)) {
521
+ warn(`unknown testing tool '${tool}' — using ${TESTING_PRIMARY}`);
522
+ tool = TESTING_PRIMARY;
523
+ }
524
+ const project_url = tool === 'none' ? null : (await ask(' project/config reference (blank to set later)', '')) || null;
525
+ registerTesting(root, { tool, project_url, today: opts.today ?? null });
526
+ ok(tool === 'none'
527
+ ? `wrote ${PROJECT_FILES.testingConfig} (artifacts-only)`
528
+ : `wrote ${PROJECT_FILES.testingConfig} (${tool})`);
529
+ }
530
+
531
+ // Connect a learning tool (DeepTutor-first, pluggable; the learning layer tutors team members here)
532
+ S('Connect a learning tool (deeptutor / none)');
533
+ guide([
534
+ 'Lets any team member invoke yad-learn to be tutored in-context. deeptutor (a CLI), or none.',
535
+ 'Skipping is safe — yad-learn tutors via the harness model (harness-native).',
536
+ ]);
537
+ if (exists(learningPath) && !(await askYesNo('learning.json exists — reconfigure?', false))) {
538
+ info('keeping existing .sdlc/learning.json');
539
+ } else {
540
+ let tool = (await ask(`Learning tool (${LEARNING_TOOLS.join('/')}/none)`, LEARNING_PRIMARY)).toLowerCase();
541
+ if (![...LEARNING_TOOLS, 'none'].includes(tool)) {
542
+ warn(`unknown learning tool '${tool}' — using ${LEARNING_PRIMARY}`);
543
+ tool = LEARNING_PRIMARY;
544
+ }
545
+ registerLearning(root, { tool, today: opts.today ?? null });
546
+ ok(tool === 'none'
547
+ ? `wrote ${PROJECT_FILES.learningConfig} (harness-native)`
548
+ : `wrote ${PROJECT_FILES.learningConfig} (${tool})`);
445
549
  }
446
- registerLearning(root, { tool, today: opts.today ?? null });
447
- ok(tool === 'none'
448
- ? `wrote ${PROJECT_FILES.learningConfig} (harness-native)`
449
- : `wrote ${PROJECT_FILES.learningConfig} (${tool})`);
550
+ } else {
551
+ // Deferred: record any not-yet-present tool as none (degrades gracefully). Existing connections kept.
552
+ S('Optional tools (design / testing / learning) — deferred');
553
+ guide(['Recorded as none; connect any later with the yad-connect-* skills. Existing connections are kept.']);
554
+ if (!exists(designPath)) registerDesign(root, { tool: 'none', project_url: null, today: opts.today ?? null });
555
+ if (!exists(testingPath)) registerTesting(root, { tool: 'none', project_url: null, today: opts.today ?? null });
556
+ if (!exists(learningPath)) registerLearning(root, { tool: 'none', today: opts.today ?? null });
557
+ info('design / testing / learning recorded as none (connect later)');
450
558
  }
451
559
 
452
- // 7. Connect code repos
453
- step(7, total, 'Connect code repos');
560
+ // Connect code repos
561
+ S(repo_layout === 'monorepo' ? 'Connect your code repo (monorepo)' : 'Connect code repos');
562
+ guide(repo_layout === 'monorepo'
563
+ ? [
564
+ 'One repo holds all the code; the contract lives in the hub and stories tag this single repo.',
565
+ codebase === 'greenfield' ? 'Greenfield: no code yet — the repomix code-pack step is skipped.' : 'Brownfield: the repo is packed so the front phases see what already exists.',
566
+ ]
567
+ : [
568
+ 'Register each code repo the feature touches; stories get tagged with the repos that implement them.',
569
+ 'Per repo: name → path (inside this project) → platform → domain owner(s).',
570
+ codebase === 'greenfield' ? 'Greenfield: no code yet — the repomix code-pack step is skipped.' : 'Brownfield: each repo is packed so the front phases see what already exists.',
571
+ ]);
454
572
  const regPath = path.join(root, PROJECT_FILES.reposRegistry);
455
573
  const registry = readJSON(regPath, { repos: [] });
456
574
  const known = new Set(registry.repos.map((r) => r.name));
575
+ const greenfield = codebase === 'greenfield';
576
+ const mono = repo_layout === 'monorepo';
457
577
  if (await askYesNo(`Connect a code repo? ${c.dim(`(${registry.repos.length} already registered)`)}`, registry.repos.length === 0)) {
458
578
  for (;;) {
459
579
  const name = await ask(' repo name (blank to finish)', '');
@@ -463,26 +583,28 @@ export async function runSetup(root, opts = {}) {
463
583
  if (!insideRoot(root, rpath)) { warn(`${rpath} resolves outside the project root — skipped`); continue; }
464
584
  const detected = run('git', ['remote', 'get-url', 'origin'], { cwd: path.resolve(root, rpath) });
465
585
  const platform = (await ask(' platform (github/gitlab)', detectPlatform(detected.ok ? detected.stdout : '') || 'github')).toLowerCase();
466
- // A repo can have one or more domain owners; reviewers/owners can also be scoped to it. Names
467
- // refer to roster `name`s; each grant is written into that person's per-scope roles map.
468
- const domain_owners = parseList(await ask(' domain owner(s) (yad names, space-separated)', ''));
469
- const repoReviewers = parseList(await ask(' repo reviewer(s) (yad names, space-separated; blank to skip)', ''));
470
- const repoOwners = parseList(await ask(' repo owner(s) (yad names, space-separated; blank to skip)', ''));
586
+ // Domain owners route the per-repo review. Solo (no roster) and monorepo (one repo = one owner)
587
+ // skip these prompts there is no second person to route to.
588
+ const domain_owners = solo || mono ? [] : parseList(await ask(' domain owner(s) (yad names, space-separated)', ''));
589
+ const repoReviewers = solo || mono ? [] : parseList(await ask(' repo reviewer(s) (yad names, space-separated; blank to skip)', ''));
590
+ const repoOwners = solo || mono ? [] : parseList(await ask(' repo owner(s) (yad names, space-separated; blank to skip)', ''));
471
591
  const default_branch = await ask(' default branch', 'main');
472
- const repo = registerRepo(root, registry, { name, rpath, platform, domain_owners, default_branch, today: opts.today ?? null });
592
+ const repo = registerRepo(root, registry, { name, rpath, platform, domain_owners, default_branch, today: opts.today ?? null, pack: !greenfield });
473
593
  if (!repo) continue;
474
594
  addRepoRoles(root, name, { 'domain-owner': domain_owners, reviewer: repoReviewers, owner: repoOwners });
475
595
  known.add(name);
476
596
  ok(`registered ${name}`);
477
- packRepo(root, repo);
597
+ if (greenfield) info(`${name}: greenfield — skipped repomix pack (run \`yad repo refresh ${name}\` once it has code)`);
598
+ else packRepo(root, repo);
599
+ if (mono) { info('monorepo — one repo connected; stop here'); break; }
478
600
  }
479
601
  }
480
602
 
481
- // 7b. Assign/update roles for ALREADY-connected repos. The connect loop above only prompts for the
482
- // repos you add now; this closes the gap so a member's domain-owner/reviewer/owner role on a repo
483
- // connected in an earlier run can be set without reconnecting. Mirrors `yad roster` (repo-driven).
603
+ // Assign/update roles for ALREADY-connected repos. Skipped in solo mode (no roster). The connect loop
604
+ // above only prompts for repos you add now; this closes the gap so a member's role on a repo connected
605
+ // in an earlier run can be set without reconnecting. Mirrors `yad roster` (repo-driven).
484
606
  const hub7 = readJSON(hubPath, null);
485
- if (registry.repos.length && hub7 && Array.isArray(hub7.roster) && hub7.roster.length
607
+ if (!solo && registry.repos.length && hub7 && Array.isArray(hub7.roster) && hub7.roster.length
486
608
  && await askYesNo('Assign/update roles for connected repos?', false)) {
487
609
  for (const member of hub7.roster) {
488
610
  if (!(await askYesNo(` edit ${member.name}'s repo roles?`, false))) continue;
@@ -496,8 +618,9 @@ export async function runSetup(root, opts = {}) {
496
618
  }
497
619
  }
498
620
 
499
- // 8. Wire each connected repo + the hub itself
500
- step(8, total, 'Wire connected repos + the hub (CI gates, PR template, comment scaffold, gate-sync)');
621
+ // Wire each connected repo + the hub itself
622
+ S('Wire connected repos + the hub (CI gates, PR template, comment scaffold, gate-sync)');
623
+ guide(['Installs the CI safety gates, PR/MR template, and gate-sync — automatic, no input needed.']);
501
624
  if (registry.repos.length === 0) info('no repos to wire');
502
625
  for (const repo of registry.repos) {
503
626
  log(` ${c.bold(repo.name)} ${c.dim(`(${repo.platform})`)}`);
@@ -516,8 +639,9 @@ export async function runSetup(root, opts = {}) {
516
639
  // author allowlists for the verified-commits gate (hub + every repo), from the roster emails
517
640
  applyActions(authorsActions(root, registry.repos), { force: true });
518
641
 
519
- // 9. Optional CodeRabbit
520
- step(9, total, 'AI review (CodeRabbit)');
642
+ // Optional CodeRabbit
643
+ S('AI review (CodeRabbit)');
644
+ guide(['Advisory AI first-pass on PRs — never the authority. Opt in per repo; safe to skip.']);
521
645
  for (const repo of registry.repos) {
522
646
  const cr = path.join(path.resolve(root, repo.path), '.coderabbit.yaml');
523
647
  if (exists(cr)) { info(`${repo.name}: .coderabbit.yaml present`); continue; }
@@ -527,13 +651,25 @@ export async function runSetup(root, opts = {}) {
527
651
  }
528
652
  }
529
653
 
530
- // 10. Summary + version stamp
531
- step(10, total, 'Done');
654
+ // Summary + version stamp
655
+ S('Done');
532
656
  writeJSON(path.join(root, PROJECT_FILES.version), { version: VERSION, ideTargets, updatedAt: opts.today ?? null });
533
657
  ok(`stamped ${PROJECT_FILES.version} (v${VERSION})`);
534
658
  log('');
535
- log(c.bold('Next AI-only steps (run in Claude Code):'));
536
- hand('generate code-maps: run `yad-connect-repos` for each connected repo');
659
+ // Tailored fastest path to the first epic, by profile.
660
+ log(c.bold('Next:'));
661
+ if (codebase === 'brownfield' && registry.repos.length) {
662
+ hand('capture what already exists first: run `yad-backfill`, then your first epic with `yad-epic`');
663
+ } else {
664
+ hand('author your first epic: run `yad-epic`');
665
+ }
666
+ hand('your single next action, anytime: `yad next`');
667
+ if (!solo && !(readJSON(hubPath, null)?.roster || []).length) {
668
+ hand('add reviewers when ready: `yad roster add <login>` (an owner + 1 reviewer passes a gate)');
669
+ }
670
+ log('');
671
+ log(c.bold('Then — AI-only steps (run in Claude Code):'));
672
+ if (registry.repos.length) hand('generate code-maps: run `yad-connect-repos` for each connected repo');
537
673
  const design = readJSON(designPath, null);
538
674
  if (design && design.tool && design.tool !== 'none') {
539
675
  hand(`confirm the design tool: run \`yad-connect-design\` to detect the ${design.tool} MCP (or it degrades to markdown-only)`);
@@ -546,7 +682,6 @@ export async function runSetup(root, opts = {}) {
546
682
  if (learning && learning.tool && learning.tool !== 'none') {
547
683
  hand(`confirm the learning tool: run \`yad-connect-learning\` to detect the ${learning.tool} CLI (or it degrades to harness-native)`);
548
684
  }
549
- hand('author your first epic: run `yad-epic`');
550
685
  log('');
551
686
  log(c.dim('Re-run anytime: `yad check` (report) / `yad check --fix` (reconcile).'));
552
687
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "2.12.0",
3
+ "version": "2.14.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). A BMAD module + 30 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
@@ -31,6 +31,14 @@ defaults:
31
31
  # Team review gate defaults (build plan §3 piece 2, §4).
32
32
  review_gate:
33
33
  default_reviewers: 1 # non-owner reviewers required (in addition to 1 owner) => owner + 1 reviewer
34
+ # Solo mode (a lone developer): a relaxed, opt-in safety guarantee, recorded per-project in
35
+ # .sdlc/hub.json (`solo: true`). On GitHub you CANNOT approve your own PR, so requiring an approval
36
+ # would deadlock a solo user. Solo waives the APPROVAL requirement only — the review PR/MR and its
37
+ # MERGE stay (CI still runs on the PR, and the merge is what advances the step). Net: the gate passes
38
+ # on `merged + all comment threads resolved`, no approval needed. NOT a default; team gates are
39
+ # unchanged. (Branch protection must not "require approvals", or the solo dev's own merge is blocked —
40
+ # `yad doctor` warns when it does.)
41
+ solo: false
34
42
  escalate_when: [contract, auth, payments] # escalate to domain owners
35
43
  # PR-driven automation (the `yad gate` CLI). With a hub platform, the review rides the per-step
36
44
  # PR/MR: `yad gate sync` maps platform reviews/threads into the file ledger and the step
@@ -26,9 +26,12 @@ engine (never typed by hand); front steps are locked to `human_approve`.
26
26
  ## On Activation
27
27
 
28
28
  ### Step 1 — Get the idea
29
- Ask the user for a one-line feature idea if not provided. If `.sdlc/state.json` already exists for the
30
- target epic, analysis was already seeded (or the epic is past it) — stop and point the user at
31
- `yad-status`. The entry point seeds state exactly once.
29
+ Ask the user for a one-line feature idea if not provided.
30
+
31
+ **Precondition gate (rail):** analysis is the optional first entry point and seeds state exactly once.
32
+ If `.sdlc/state.json` already exists for the target epic, run `yad next EP-<slug> --check analysis`; if it
33
+ exits non-zero, **STOP** and point the user at `yad next EP-<slug>` (the epic is past analysis). If it
34
+ exits zero, resume analysis for that epic. When no `state.json` exists yet, proceed and seed state.
32
35
 
33
36
  ### Step 2 — Shape the idea (assist: analyst)
34
37
  Adopt the **analyst** lens (`bmad-agent-analyst`, Mary) to pressure-test the idea in depth: who is the
@@ -23,9 +23,14 @@ shared cross-repo surface at charter altitude; front steps stay locked to `human
23
23
  ## On Activation
24
24
 
25
25
  ### Step 1 — Resolve the epic and check the gate
26
- Resolve the `EP-<slug>` (ask if not provided). Read `{project-root}/epics/EP-<slug>/.sdlc/state.json`.
27
- Only proceed when `currentStep == "architecture"` and that step's `status == "in_progress"` (the epic
28
- review must already have passed). If not, stop and point the user at `yad-status` / the gate.
26
+ Resolve the `EP-<slug>` (ask if not provided).
27
+
28
+ **Precondition gate (rail):** run `yad next EP-<slug> --check architecture` first. If it exits non-zero,
29
+ **STOP** — surface the blocker it prints and point the user at `yad next EP-<slug>`. Do not author until
30
+ it passes. (The check is the authoritative rail; the description below explains what it enforces.)
31
+
32
+ This passes when `architecture` is the next runnable step per the state sequence — every prior step
33
+ (through the epic review) is `done` and `architecture` is not already `done`.
29
34
 
30
35
  ### Step 1b — Open the authoring branch
31
36
  Open the architecture authoring branch `architecture/EP-<slug>` per the shared procedure
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yad-docs-overview
3
- description: 'Generates the project-level SDLC-overview interactive site — the same React/Vite/Tailwind shell as the per-epic docs — showing every yadflow stage from setup → ship: the pipeline as a flow canvas, each skill/gate as a flow step, the durable .sdlc state objects as system components, and the lenses as stakeholder roles. Themed with yadflow''s own brand palette for continuity, built from config.yaml + module-help.csv + the overview diagram. Folds the legacy hand-maintained docs/index.html report into the site as report.html (linked from the nav) and deploys via `yad docs deploy --overview`. This is project documentation, not a gated state — it never touches any epic''s state or approvals. Use when the user says "generate the overview site", "build the SDLC overview docs", or after the pipeline (module-help.csv / config.yaml / skill count) changes.'
3
+ description: 'Generates the project-level SDLC-overview interactive site — the same React/Vite/Tailwind shell as the per-epic docs — showing every yadflow stage from setup → ship: the pipeline as a flow canvas, each skill/gate as a flow step, the durable .sdlc state objects as system components, and the lenses as stakeholder roles. Themed with yadflow''s own brand palette for continuity, built from config.yaml + module-help.csv + the overview diagram. The hand-maintained report is the MAIN documentation at the Pages root (report.html, also served as index.html); the interactive SPA mounts under `app/` and is reached from it (and links back). Deploys via `yad docs deploy --overview`. This is project documentation, not a gated state — it never touches any epic''s state or approvals. Use when the user says "generate the overview site", "build the SDLC overview docs", or after the pipeline (module-help.csv / config.yaml / skill count) changes.'
4
4
  ---
5
5
 
6
6
  # SDLC — Author the Overview Site (project-level, the pipeline as a living map)
@@ -8,8 +8,10 @@ description: 'Generates the project-level SDLC-overview interactive site — the
8
8
  **Goal:** Render the **whole yadflow pipeline** — every stage from setup → ship — as an interactive site,
9
9
  reusing the same shell as the per-epic docs (`skills/yad-docs/templates/app/`). Where `yad-docs`
10
10
  animates one epic's flows, this animates the **workflow itself**: the front gates, the build half, the
11
- automation dial, the setup connectors. It is the regenerable successor to the hand-maintained
12
- overview report, which is folded into this site as `public/report.html`.
11
+ automation dial, the setup connectors. The hand-maintained overview report stays the **main
12
+ documentation at the Pages root** (`<base>/`, served from `public/report.html` and `public/index.html`);
13
+ this interactive SPA mounts under `<base>/app/` and is reached from the report — the report links
14
+ forward to `app/`, the app links back to the report root.
13
15
 
14
16
  This is **project documentation, not a gated state** — there is no epic, no `state.json`, no approvals.
15
17
  It only reads the pipeline definition and writes a project-level site. When a docs target is connected
@@ -70,7 +72,8 @@ determinism rules as `yad-docs`: stable-ID sort by skill pipeline order / phase,
70
72
  timestamps in the data files), theme the `:root` of `index.css` from **yadflow's brand palette** — the
71
73
  the legacy report's `:root`: `--accent: #2471a3` and the node colors (`--artifact-*`, `--gate-*`,
72
74
  `--earns-*`, `--locked-*`, `--sentinel-*`) — and substitute the Vite base from `.sdlc/docs.json`
73
- `basePath` (the overview sits at the base root, e.g. `/<repo>/`).
75
+ `basePath` **with `app/` appended** (the SPA mounts under `<base>/app/`, e.g. `/<repo>/app/`, so the
76
+ report can own the root). `siteBasePath(docs, { overview: true })` in `cli/docs.mjs` computes this.
74
77
 
75
78
  ### Step 4 — Write the overview build manifest (the staleness baseline)
76
79
  Write `docs/sdlc-site/.docs-build.json` — `yad-docs-sync` compares against it:
@@ -91,13 +94,16 @@ doc-shell upgrade triggers a rebuild). `skillCount` rides along in the manifest
91
94
  — it is **not** a separate hash input, since `module-help.csv` already moves whenever the skill set does.
92
95
  Not per-epic artifacts/repo heads.
93
96
 
94
- ### Step 5 — Fold the legacy report into the site
95
- Relocate the hand-maintained static report into the generated site as `docs/sdlc-site/public/report.html`
96
- (Vite copies `public/` verbatim into `dist/`, so it publishes alongside the app at `<base>/report.html`),
97
- and link it from the app nav (a "Full report" link in `TopNavBar`). The interactive overview becomes the
98
- primary documentation and the legacy report rides along as its detailed companion no orphaned
99
- `docs/index.html` at the repo root. This generalizes the standing rule that feature work hand-updates the
100
- report: the overview site now **regenerates** instead.
97
+ ### Step 5 — The report is the main documentation; the SPA is reached from it
98
+ The hand-maintained static report lives at `docs/sdlc-site/public/report.html` and is the **primary
99
+ documentation at the Pages root**. The deploy (`BUILD_PUBLIC` in `cli/docs.mjs`, mirrored by the Pages CI
100
+ workflow) copies it to **both** `public/index.html` (the landing `<base>/`) and `public/report.html`
101
+ (so the `<base>/report.html` URL keeps working), and copies the built SPA into `public/app/`. Wire the
102
+ two cross-links: the report links **forward** to the interactive map with a relative `app/` href (its
103
+ hero CTA); the app's `TopNavBar` "Full report" link points **back** to the report root
104
+ (`import.meta.env.BASE_URL` with the trailing `app/` stripped). No orphaned `docs/index.html` at the repo
105
+ root. This generalizes the standing rule that feature work hand-updates the report: the overview SPA now
106
+ **regenerates** instead, while the report stays the front door.
101
107
 
102
108
  ### Step 6 — Build / deploy (`action`)
103
109
  - `action: generate` (default) — generate source + manifest; stop.
@@ -106,8 +112,9 @@ report: the overview site now **regenerates** instead.
106
112
 
107
113
  ### Step 7 — Stop. Report (no gate, no epic)
108
114
  Report: the site path (`docs/sdlc-site/`), the data files produced, that the theme is the yadflow brand
109
- palette, the deploy URL or "build-only", the staleness baseline, and that the legacy report is folded in
110
- at `public/report.html` (linked from the nav). Never touches any epic state.
115
+ palette, the deploy URL or "build-only", the staleness baseline, and that the report is the main
116
+ documentation at `<base>/` (`public/index.html` + `public/report.html`) with the interactive SPA mounted
117
+ under `<base>/app/` and cross-linked. Never touches any epic state.
111
118
 
112
119
  ## Hard rules
113
120
 
@@ -40,7 +40,10 @@ exists for the idea.
40
40
 
41
41
  Either mode runs Step 3b (branch), Step 4 (write the epic), and Step 6 (stop at the gate).
42
42
 
43
- If `state.json` exists but `currentStep != "epic"`, stop and point the user at `yad-status` / the gate.
43
+ **Precondition gate (rail):** if `state.json` already exists (analysis-ran or re-entry), run
44
+ `yad next EP-<slug> --check epic` — if it exits non-zero, **STOP** and surface the blocker, pointing the
45
+ user at `yad next EP-<slug>`. When no `state.json` exists yet, this is the greenfield entry point — the
46
+ check is not applicable, so proceed and seed state.
44
47
 
45
48
  ### Step 2 — Shape the idea (assist: analyst) — or read the analysis
46
49
  - **Analysis skipped:** ask the user for a one-line feature idea if not provided, then adopt the
@@ -24,9 +24,14 @@ There is **no `sm` agent** (Phase 0 Deviation 1): the `pm` lens breaks down the
24
24
  ## On Activation
25
25
 
26
26
  ### Step 1 — Resolve the epic and check the gate
27
- Resolve the `EP-<slug>` (ask if not provided). Read `.sdlc/state.json`. Only proceed when
28
- `currentStep == "stories"` and that step's `status == "in_progress"` (the UI review must already have
29
- passed). If not, stop and point the user at `yad-status` / the gate.
27
+ Resolve the `EP-<slug>` (ask if not provided).
28
+
29
+ **Precondition gate (rail):** run `yad next EP-<slug> --check stories` first. If it exits non-zero,
30
+ **STOP** — surface the blocker it prints and point the user at `yad next EP-<slug>`. Do not author until
31
+ it passes.
32
+
33
+ This passes when `stories` is the next runnable step per the state sequence — every prior step
34
+ (through the UI review) is `done` and `stories` is not already `done`.
30
35
 
31
36
  ### Step 1b — Open the authoring branch
32
37
  Open the stories authoring branch `stories/EP-<slug>` per the shared procedure
@@ -36,11 +36,16 @@ the Markdown artifact only — the testing tool is additive, exactly like the de
36
36
  ## On Activation
37
37
 
38
38
  ### Step 1 — Resolve the epic and check the track
39
- Resolve the `EP-<slug>` (ask if not provided). Read `.sdlc/state.json`. Only proceed when the
40
- **`test-cases` step's `status == "in_progress"`** — it opens when `stories-review` passes (the epic is
41
- already `ready-for-build` by then; `currentStep` stays there because this is a parallel track, so do
42
- **not** gate on `currentStep`). If `test-cases` is still `blocked`, the stories review has not passed
43
- stop and point the user at `yad-status` / the gate.
39
+ Resolve the `EP-<slug>` (ask if not provided).
40
+
41
+ **Precondition gate (rail):** run `yad next EP-<slug> --check test-cases` first. If it exits non-zero,
42
+ **STOP** surface the blocker it prints and point the user at `yad next EP-<slug>`. Do not author until
43
+ it passes. The check is track-aware: it keys off the `test-cases` step's predecessors, **not**
44
+ `currentStep` (this is a parallel track — `currentStep` stays at `ready-for-build`).
45
+
46
+ This passes once the `test-cases` step is runnable — its predecessor `stories-review` is `done` (so the
47
+ step has opened to `in_progress`) and `test-cases` is not already `done`. While it is still `blocked`,
48
+ the stories review has not passed.
44
49
 
45
50
  ### Step 1b — Open the authoring branch
46
51
  Open the test-cases authoring branch `test-cases/EP-<slug>` per the shared procedure