yadflow 2.6.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +2 -11
  2. package/README.md +30 -5
  3. package/bin/yad.mjs +36 -1
  4. package/cli/docs.mjs +298 -0
  5. package/cli/manifest.mjs +6 -1
  6. package/cli/roster.mjs +164 -0
  7. package/cli/setup.mjs +128 -2
  8. package/package.json +3 -4
  9. package/skills/sdlc/config.yaml +19 -0
  10. package/skills/sdlc/install.sh +1 -1
  11. package/skills/sdlc/module-help.csv +4 -0
  12. package/skills/yad-connect-docs/SKILL.md +132 -0
  13. package/skills/yad-connect-docs/references/docs-registry.md +74 -0
  14. package/skills/yad-connect-repos/SKILL.md +4 -0
  15. package/skills/yad-connect-repos/references/hub-config.md +3 -1
  16. package/skills/yad-docs/SKILL.md +159 -0
  17. package/skills/yad-docs/references/data-mapping.md +75 -0
  18. package/skills/yad-docs/references/theme-map.md +69 -0
  19. package/skills/yad-docs/templates/app/README.md +31 -0
  20. package/skills/yad-docs/templates/app/eslint.config.js +23 -0
  21. package/skills/yad-docs/templates/app/index.html +17 -0
  22. package/skills/yad-docs/templates/app/package-lock.json +4030 -0
  23. package/skills/yad-docs/templates/app/package.json +35 -0
  24. package/skills/yad-docs/templates/app/public/favicon.svg +28 -0
  25. package/skills/yad-docs/templates/app/public/logo.svg +39 -0
  26. package/skills/yad-docs/templates/app/public/vite.svg +1 -0
  27. package/skills/yad-docs/templates/app/src/App.tsx +98 -0
  28. package/skills/yad-docs/templates/app/src/components/Auth/LoginPage.tsx +101 -0
  29. package/skills/yad-docs/templates/app/src/components/Canvas/AnimatedMessage.tsx +101 -0
  30. package/skills/yad-docs/templates/app/src/components/Canvas/ConnectionLine.tsx +90 -0
  31. package/skills/yad-docs/templates/app/src/components/Canvas/FlowCanvas.tsx +216 -0
  32. package/skills/yad-docs/templates/app/src/components/Canvas/SystemComponent.tsx +153 -0
  33. package/skills/yad-docs/templates/app/src/components/Controls/PlaybackBar.tsx +284 -0
  34. package/skills/yad-docs/templates/app/src/components/Controls/StepDetail.tsx +167 -0
  35. package/skills/yad-docs/templates/app/src/components/DetailPanel/HandlerLogicSnippet.tsx +41 -0
  36. package/skills/yad-docs/templates/app/src/components/DetailPanel/RequestPayloadPreview.tsx +46 -0
  37. package/skills/yad-docs/templates/app/src/components/DetailPanel/RightPanel.tsx +88 -0
  38. package/skills/yad-docs/templates/app/src/components/DetailPanel/StatusCard.tsx +76 -0
  39. package/skills/yad-docs/templates/app/src/components/DetailPanel/TriggerEventCard.tsx +45 -0
  40. package/skills/yad-docs/templates/app/src/components/DocLayout/DocPageShell.tsx +80 -0
  41. package/skills/yad-docs/templates/app/src/components/DocLayout/DocSectionCard.tsx +55 -0
  42. package/skills/yad-docs/templates/app/src/components/DocLayout/DocTableOfContents.tsx +79 -0
  43. package/skills/yad-docs/templates/app/src/components/DocLayout/RoleCard.tsx +67 -0
  44. package/skills/yad-docs/templates/app/src/components/DocSections/ApiReferenceSection.tsx +108 -0
  45. package/skills/yad-docs/templates/app/src/components/DocSections/CancelabilitySection.tsx +73 -0
  46. package/skills/yad-docs/templates/app/src/components/DocSections/CriticalRunbookSection.tsx +177 -0
  47. package/skills/yad-docs/templates/app/src/components/DocSections/DataMigrationSection.tsx +102 -0
  48. package/skills/yad-docs/templates/app/src/components/DocSections/DbSchemaSection.tsx +98 -0
  49. package/skills/yad-docs/templates/app/src/components/DocSections/DeploymentGuideSection.tsx +104 -0
  50. package/skills/yad-docs/templates/app/src/components/DocSections/DriverIntegrationSection.tsx +127 -0
  51. package/skills/yad-docs/templates/app/src/components/DocSections/ExecutiveSummarySection.tsx +69 -0
  52. package/skills/yad-docs/templates/app/src/components/DocSections/FlowOverviewSection.tsx +73 -0
  53. package/skills/yad-docs/templates/app/src/components/DocSections/FlowPathsChecklistSection.tsx +96 -0
  54. package/skills/yad-docs/templates/app/src/components/DocSections/MiddlewareChainSection.tsx +107 -0
  55. package/skills/yad-docs/templates/app/src/components/DocSections/MonitoringAlertingSection.tsx +106 -0
  56. package/skills/yad-docs/templates/app/src/components/DocSections/NotificationLocalizationSection.tsx +102 -0
  57. package/skills/yad-docs/templates/app/src/components/DocSections/PMRoadmapSection.tsx +133 -0
  58. package/skills/yad-docs/templates/app/src/components/DocSections/PerformanceTestingSection.tsx +91 -0
  59. package/skills/yad-docs/templates/app/src/components/DocSections/RiderIntegrationSection.tsx +99 -0
  60. package/skills/yad-docs/templates/app/src/components/DocSections/SecuritySection.tsx +74 -0
  61. package/skills/yad-docs/templates/app/src/components/DocSections/StatusMachineSection.tsx +90 -0
  62. package/skills/yad-docs/templates/app/src/components/DocSections/TestPlanSection.tsx +163 -0
  63. package/skills/yad-docs/templates/app/src/components/Logs/SystemLogsTerminal.tsx +126 -0
  64. package/skills/yad-docs/templates/app/src/components/Navigation/TopNavBar.tsx +90 -0
  65. package/skills/yad-docs/templates/app/src/components/Reference/BullMQJobsList.tsx +60 -0
  66. package/skills/yad-docs/templates/app/src/components/Reference/DecisionTreeView.tsx +49 -0
  67. package/skills/yad-docs/templates/app/src/components/Reference/DeeplinkActionsChips.tsx +69 -0
  68. package/skills/yad-docs/templates/app/src/components/Reference/DriverUIStatesTable.tsx +61 -0
  69. package/skills/yad-docs/templates/app/src/components/Reference/FeatureFlagMatrix.tsx +73 -0
  70. package/skills/yad-docs/templates/app/src/components/Reference/RiderUIStatesTable.tsx +61 -0
  71. package/skills/yad-docs/templates/app/src/components/Reference/RulesLegendPanel.tsx +217 -0
  72. package/skills/yad-docs/templates/app/src/components/Reference/StakeholderToggle.tsx +41 -0
  73. package/skills/yad-docs/templates/app/src/components/Reference/TroubleshootingSection.tsx +93 -0
  74. package/skills/yad-docs/templates/app/src/components/Sidebar/PathSelector.tsx +148 -0
  75. package/skills/yad-docs/templates/app/src/components/Sidebar/SidebarFooter.tsx +40 -0
  76. package/skills/yad-docs/templates/app/src/components/Sidebar/StepList.tsx +234 -0
  77. package/skills/yad-docs/templates/app/src/components/shared/Badge.tsx +28 -0
  78. package/skills/yad-docs/templates/app/src/components/shared/CommandPalette.tsx +213 -0
  79. package/skills/yad-docs/templates/app/src/components/shared/Icon.tsx +21 -0
  80. package/skills/yad-docs/templates/app/src/components/shared/Tooltip.tsx +42 -0
  81. package/skills/yad-docs/templates/app/src/data/components.ts +74 -0
  82. package/skills/yad-docs/templates/app/src/data/docSections.ts +231 -0
  83. package/skills/yad-docs/templates/app/src/data/paths.ts +2319 -0
  84. package/skills/yad-docs/templates/app/src/data/referenceData.ts +392 -0
  85. package/skills/yad-docs/templates/app/src/data/roles.ts +145 -0
  86. package/skills/yad-docs/templates/app/src/data/types.ts +79 -0
  87. package/skills/yad-docs/templates/app/src/hooks/useAnimationQueue.ts +41 -0
  88. package/skills/yad-docs/templates/app/src/hooks/usePlayback.ts +100 -0
  89. package/skills/yad-docs/templates/app/src/hooks/useStakeholderFilter.ts +10 -0
  90. package/skills/yad-docs/templates/app/src/index.css +121 -0
  91. package/skills/yad-docs/templates/app/src/main.tsx +13 -0
  92. package/skills/yad-docs/templates/app/src/pages/RoleSelectPage.tsx +34 -0
  93. package/skills/yad-docs/templates/app/src/pages/StakeholderDocPage.tsx +98 -0
  94. package/skills/yad-docs/templates/app/src/pages/SubPathDetailPage.tsx +282 -0
  95. package/skills/yad-docs/templates/app/src/store/useAuthStore.ts +42 -0
  96. package/skills/yad-docs/templates/app/src/store/useFlowStore.ts +197 -0
  97. package/skills/yad-docs/templates/app/src/utils/iconMap.ts +46 -0
  98. package/skills/yad-docs/templates/app/tsconfig.app.json +28 -0
  99. package/skills/yad-docs/templates/app/tsconfig.json +7 -0
  100. package/skills/yad-docs/templates/app/tsconfig.node.json +26 -0
  101. package/skills/yad-docs/templates/app/vite.config.ts +10 -0
  102. package/skills/yad-docs-overview/SKILL.md +131 -0
  103. package/skills/yad-docs-overview/references/pipeline-model.md +102 -0
  104. package/skills/yad-docs-sync/SKILL.md +99 -0
  105. package/skills/yad-docs-sync/references/staleness.md +81 -0
  106. package/skills/yad-hub-bridge/references/login-roster.md +1 -0
  107. package/docs/index.html +0 -1323
package/cli/roster.mjs ADDED
@@ -0,0 +1,164 @@
1
+ // `yad roster` — manage the reviewer roster (.sdlc/hub.json) and per-repo roles at any time.
2
+ // The roster maps a platform login -> SDLC name + a per-scope roles map; it is the only thing that lets
3
+ // the review gate attribute approvals and route per-repo domain-owner reviewers. This is the standalone
4
+ // counterpart of the `yad setup` roster step and the `yad-connect-repos action: roster` skill action.
5
+ // Repo-driven: `add`/`edit` walks the connected repos from repos.json so roles are assigned against what
6
+ // is actually connected, not against repo names the user has to remember. Granting/revoking a
7
+ // `domain-owner` keeps repos.json `domain_owners` in sync so the gate never drifts from the roster.
8
+ import path from 'node:path';
9
+ import { c, log, ok, info, warn, hand, fail, ask, askYesNo, readJSON, writeJSON } from './lib.mjs';
10
+ import { PROJECT_FILES } from './manifest.mjs';
11
+ import { rolesForScope } from './platform.mjs';
12
+ import { parseRolesSpec, upsertRosterEntry, addRepoRoles, removeRepoRole, setRepoDomainOwners, reconcileRepoRoles } from './setup.mjs';
13
+
14
+ const ROLES = ['owner', 'reviewer', 'domain-owner'];
15
+
16
+ const loadHub = (root) => readJSON(path.join(root, PROJECT_FILES.hubConfig), null);
17
+ const loadRepos = (root) => readJSON(path.join(root, PROJECT_FILES.reposRegistry), { repos: [] }).repos || [];
18
+ const ownersOf = (repo) => (Array.isArray(repo.domain_owners) ? repo.domain_owners : (repo.domain_owner ? [repo.domain_owner] : []));
19
+
20
+ // `list` — every member with their per-scope roles, plus a drift check between hub.json roles and
21
+ // repos.json domain_owners (the two should agree; the sync on grant/revoke keeps them aligned).
22
+ function rosterList(root) {
23
+ const hub = loadHub(root);
24
+ if (!hub || !Array.isArray(hub.roster) || !hub.roster.length) {
25
+ warn('no roster yet (.sdlc/hub.json) — add one with `yad roster add <login>` or `yad setup`');
26
+ return { members: 0 };
27
+ }
28
+ const repos = loadRepos(root);
29
+ log(c.bold('\nreviewer roster'));
30
+ for (const e of hub.roster) {
31
+ const flag = e.unverified ? c.yellow(' (unverified)') : '';
32
+ log(` ${c.bold(e.name || e.login)} ${c.dim(`@${e.login}`)}${e.email ? c.dim(` <${e.email}>`) : ''}${flag}`);
33
+ const hubRoles = rolesForScope(e, 'hub');
34
+ log(` hub: ${hubRoles.length ? hubRoles.join(', ') : c.dim('—')}`);
35
+ for (const r of repos) {
36
+ const rr = rolesForScope(e, r.name);
37
+ if (rr.length) log(` ${r.name}: ${rr.join(', ')}`);
38
+ }
39
+ }
40
+ const drift = [];
41
+ for (const r of repos) {
42
+ const owners = ownersOf(r);
43
+ for (const nm of owners) {
44
+ const e = hub.roster.find((x) => x.name === nm);
45
+ if (!e || !rolesForScope(e, r.name).includes('domain-owner')) {
46
+ drift.push(`${nm} owns ${r.name} in repos.json but has no domain-owner role in hub.json`);
47
+ }
48
+ }
49
+ for (const e of hub.roster) {
50
+ if (rolesForScope(e, r.name).includes('domain-owner') && !owners.includes(e.name)) {
51
+ drift.push(`${e.name} has domain-owner for ${r.name} in hub.json but is missing from repos.json domain_owners`);
52
+ }
53
+ }
54
+ }
55
+ if (drift.length) { log(''); for (const d of drift) warn(d); }
56
+ return { members: hub.roster.length, repos: repos.length, drift: drift.length };
57
+ }
58
+
59
+ // The repo-driven walk: for each connected repo show the member's current role and offer to set it.
60
+ async function repoWalk(root, entry) {
61
+ const repos = loadRepos(root);
62
+ if (!repos.length) { info('no connected repos to assign roles for (.sdlc/repos.json) — connect one with `yad setup`'); return; }
63
+ for (const r of repos) {
64
+ const cur = rolesForScope(entry, r.name);
65
+ if (!(await askYesNo(` set ${entry.name}'s role on ${r.name}? (current: ${cur.length ? cur.join(', ') : 'none'})`, false))) continue;
66
+ const input = await ask(' roles (domain-owner/reviewer/owner, space-separated; blank = clear)', cur.join(' '));
67
+ const want = input.split(/\s+/).map((x) => x.trim()).filter(Boolean);
68
+ const invalid = want.filter((x) => !ROLES.includes(x));
69
+ if (invalid.length) warn(`ignoring unknown role(s): ${invalid.join(', ')} (allowed: ${ROLES.join(', ')})`);
70
+ reconcileRepoRoles(root, entry.name, r.name, cur, want.filter((x) => ROLES.includes(x)));
71
+ }
72
+ }
73
+
74
+ // Mirror any domain-owner scopes from a non-interactive `--roles` upsert into repos.json.
75
+ function syncDomainOwners(root, entry) {
76
+ for (const [scope, list] of Object.entries(entry.roles || {})) {
77
+ if (scope !== 'hub' && Array.isArray(list) && list.includes('domain-owner')) setRepoDomainOwners(root, scope, entry.name, { add: true });
78
+ }
79
+ }
80
+
81
+ // `add`/`edit <login>` — upsert by login (from flags or interactive prompts), then either apply a
82
+ // `--roles` spec directly (scriptable) or run the repo-driven walk (interactive default).
83
+ async function rosterAdd(root, login, { name, email, roles } = {}) {
84
+ if (!login) { fail('usage: yad roster add <login> [--name N] [--email E] [--roles "hub=owner,reviewer backend=domain-owner"]'); process.exitCode = 1; return {}; }
85
+ const hub = loadHub(root);
86
+ const platform = hub ? hub.platform : null;
87
+ const existing = hub && Array.isArray(hub.roster) ? hub.roster.find((e) => e.login === login) : null;
88
+ const scripted = !!roles;
89
+ let nm = name;
90
+ let em = email;
91
+ let rolesMap;
92
+ if (scripted) {
93
+ rolesMap = parseRolesSpec(roles);
94
+ } else {
95
+ nm = nm || await ask(' yad name', (existing && existing.name) || login);
96
+ em = em || await ask(' commit email (blank to skip)', (existing && existing.email) || '');
97
+ const def = rolesForScope(existing, 'hub').join(' ') || 'reviewer';
98
+ const hubRoles = (await ask(' hub roles (owner/reviewer, space-separated)', def)).split(/\s+/).filter(Boolean);
99
+ rolesMap = hubRoles.length ? { hub: hubRoles } : {};
100
+ }
101
+ const { entry, created } = upsertRosterEntry(root, { login, name: nm, email: em || undefined, roles: rolesMap, platform });
102
+ if (!entry) return {};
103
+ ok(`${created ? 'added' : 'updated'} ${entry.name} (@${login})`);
104
+ if (scripted) syncDomainOwners(root, entry);
105
+ else await repoWalk(root, entry);
106
+ return { entry: entry.name, created };
107
+ }
108
+
109
+ // `grant <name> <repo> <role...>` — scriptable per-repo grant (member must already be in the roster).
110
+ function rosterGrant(root, [name, repo, ...roles]) {
111
+ if (!name || !repo || !roles.length) { fail('usage: yad roster grant <name> <repo> <role...>'); process.exitCode = 1; return {}; }
112
+ const invalid = roles.filter((r) => !ROLES.includes(r));
113
+ if (invalid.length) { fail(`unknown role(s): ${invalid.join(', ')} (allowed: ${ROLES.join(', ')})`); process.exitCode = 1; return {}; }
114
+ const hub = loadHub(root);
115
+ if (!hub || !Array.isArray(hub.roster) || !hub.roster.some((e) => e.name === name)) {
116
+ fail(`'${name}' is not in the roster — add them first with \`yad roster add <login>\``); process.exitCode = 1; return {};
117
+ }
118
+ if (!loadRepos(root).some((r) => r.name === repo)) warn(`repo '${repo}' is not registered — the role is recorded and applies once it is connected`);
119
+ addRepoRoles(root, repo, Object.fromEntries(roles.map((r) => [r, [name]])));
120
+ if (roles.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: true });
121
+ ok(`granted ${name} ${roles.join(', ')} on ${repo}`);
122
+ return { name, repo, roles };
123
+ }
124
+
125
+ // `revoke <name> <repo> <role...>` — scriptable per-repo revoke.
126
+ function rosterRevoke(root, [name, repo, ...roles]) {
127
+ if (!name || !repo || !roles.length) { fail('usage: yad roster revoke <name> <repo> <role...>'); process.exitCode = 1; return {}; }
128
+ removeRepoRole(root, name, repo, roles);
129
+ if (roles.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: false });
130
+ ok(`revoked ${name} ${roles.join(', ')} on ${repo}`);
131
+ return { name, repo, roles };
132
+ }
133
+
134
+ // `remove <login>` — delete a member; warn (do not cascade) if still a domain owner in repos.json.
135
+ function rosterRemove(root, login) {
136
+ if (!login) { fail('usage: yad roster remove <login>'); process.exitCode = 1; return {}; }
137
+ const hubPath = path.join(root, PROJECT_FILES.hubConfig);
138
+ const hub = readJSON(hubPath, null);
139
+ if (!hub || !Array.isArray(hub.roster)) { warn('no roster to remove from (.sdlc/hub.json)'); return { removed: 0 }; }
140
+ const idx = hub.roster.findIndex((e) => e.login === login);
141
+ if (idx < 0) { warn(`no roster member with login '${login}'`); return { removed: 0 }; }
142
+ const [removed] = hub.roster.splice(idx, 1);
143
+ writeJSON(hubPath, hub);
144
+ ok(`removed ${removed.name} (@${login})`);
145
+ const refs = loadRepos(root).filter((r) => ownersOf(r).includes(removed.name)).map((r) => r.name);
146
+ if (refs.length) hand(`'${removed.name}' is still a domain owner in repos.json for: ${refs.join(', ')} — revoke with \`yad roster revoke ${removed.name} <repo> domain-owner\``);
147
+ return { removed: 1 };
148
+ }
149
+
150
+ export async function runRoster(root, { action = 'list', args = [], name, email, roles } = {}) {
151
+ switch (action) {
152
+ case 'list': return rosterList(root);
153
+ case 'add':
154
+ case 'edit': return rosterAdd(root, args[0], { name, email, roles });
155
+ case 'grant': return rosterGrant(root, args);
156
+ case 'revoke': return rosterRevoke(root, args);
157
+ case 'remove':
158
+ case 'rm': return rosterRemove(root, args[0]);
159
+ default:
160
+ fail(`unknown roster action: ${action} (list | add | edit | grant | revoke | remove)`);
161
+ process.exitCode = 1;
162
+ return {};
163
+ }
164
+ }
package/cli/setup.mjs CHANGED
@@ -11,13 +11,29 @@ import {
11
11
  moduleActions, repoActions, hubActions, authorsActions,
12
12
  legacyModuleActions, legacyRepoActions, legacyHubActions,
13
13
  } from './plan.mjs';
14
- import { validateLogin } from './platform.mjs';
14
+ import { validateLogin, rolesForScope } from './platform.mjs';
15
15
 
16
16
  // Parse a comma/space separated list into a clean, deduped array of trimmed tokens.
17
- function parseList(s) {
17
+ export function parseList(s) {
18
18
  return [...new Set((s || '').split(/[,\s]+/).map((x) => x.trim()).filter(Boolean))];
19
19
  }
20
20
 
21
+ // Parse a per-scope roles spec — `"hub=owner,reviewer backend=domain-owner"` — into the roster's
22
+ // per-scope map `{ hub: ['owner','reviewer'], backend: ['domain-owner'] }`. Tokens are whitespace
23
+ // separated; each is `scope=role[,role...]`. Malformed tokens (no `=`, empty scope/roles) are skipped.
24
+ export function parseRolesSpec(s) {
25
+ const out = {};
26
+ for (const tok of (s || '').split(/\s+/).map((x) => x.trim()).filter(Boolean)) {
27
+ const eq = tok.indexOf('=');
28
+ if (eq < 0) continue;
29
+ const scope = tok.slice(0, eq).trim();
30
+ const roles = parseList(tok.slice(eq + 1));
31
+ if (!scope || !roles.length) continue;
32
+ out[scope] = [...new Set([...(out[scope] || []), ...roles])];
33
+ }
34
+ return out;
35
+ }
36
+
21
37
  const ALL_IDES = [...IDE_FOLDER_TARGETS, '.opencode'];
22
38
 
23
39
  export function detectPlatform(remoteUrl = '') {
@@ -68,6 +84,98 @@ export function addRepoRoles(root, repo, grants = {}) {
68
84
  if (touched) writeJSON(hubPath, hub);
69
85
  }
70
86
 
87
+ // Normalize a roster entry's roles in place to the per-scope map, migrating the two legacy shapes
88
+ // (a flat array, or a single `role` string) into `roles.hub` so nothing is lost. Mirrors the
89
+ // migration addRepoRoles does; pulled out so upsert/remove share it.
90
+ function normalizeRoles(entry) {
91
+ if (!entry.roles || typeof entry.roles !== 'object' || Array.isArray(entry.roles)) {
92
+ const hub = Array.isArray(entry.roles) ? entry.roles : (entry.role ? [entry.role] : []);
93
+ entry.roles = hub.length ? { hub } : {};
94
+ delete entry.role;
95
+ }
96
+ return entry.roles;
97
+ }
98
+
99
+ // Upsert one roster member into hub.json, keyed by `login`. Deep-merges the per-scope `roles` map so
100
+ // scopes the caller did not name are preserved; sets `name`/`email` when given; validates the login
101
+ // against the hub (warn-only — a miss flags `unverified`, `checked:false` skips silently). Creates the
102
+ // hub.json shell if absent. Returns { entry, created }.
103
+ export function upsertRosterEntry(root, { login, name, email, roles = {}, platform } = {}) {
104
+ if (!login) { warn('roster upsert needs a login — skipped'); return { entry: null, created: false }; }
105
+ const hubPath = path.join(root, PROJECT_FILES.hubConfig);
106
+ const hub = readJSON(hubPath, null) || { platform: platform && platform !== 'none' ? platform : null, bridge_enabled: false, bridge: false, default_branch: 'main', roster: [] };
107
+ if (!Array.isArray(hub.roster)) hub.roster = [];
108
+ let entry = hub.roster.find((e) => e.login === login);
109
+ const created = !entry;
110
+ if (!entry) { entry = { login, name: name || login, roles: {} }; hub.roster.push(entry); }
111
+ if (name) entry.name = name;
112
+ if (email) entry.email = email;
113
+ normalizeRoles(entry);
114
+ for (const [scope, list] of Object.entries(roles || {})) {
115
+ const cur = Array.isArray(entry.roles[scope]) ? entry.roles[scope] : [];
116
+ entry.roles[scope] = [...new Set([...cur, ...list])];
117
+ }
118
+ if (email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) warn(`'${email}' does not look like an email address`);
119
+ const plat = platform || hub.platform;
120
+ if (plat && plat !== 'none') {
121
+ const v = validateLogin(plat, login);
122
+ if (v.checked && !v.exists) { warn(`'${login}' not found on ${plat} — saved as unverified`); entry.unverified = true; }
123
+ else if (v.checked && v.exists) { ok(`verified ${login} on ${plat}`); delete entry.unverified; }
124
+ }
125
+ writeJSON(hubPath, hub);
126
+ return { entry, created };
127
+ }
128
+
129
+ // Inverse of addRepoRoles: drop the named role(s) from a member's `roles[<repo>]` scope, removing the
130
+ // scope key when it empties. Member is found by yad `name` (matching addRepoRoles). Idempotent.
131
+ export function removeRepoRole(root, name, repo, roles = []) {
132
+ const hubPath = path.join(root, PROJECT_FILES.hubConfig);
133
+ const hub = readJSON(hubPath, null);
134
+ if (!hub || !Array.isArray(hub.roster)) return;
135
+ const entry = hub.roster.find((e) => e.name === name);
136
+ if (!entry) { warn(`'${name}' is not in the roster — nothing to revoke for ${repo}`); return; }
137
+ normalizeRoles(entry);
138
+ const cur = Array.isArray(entry.roles[repo]) ? entry.roles[repo] : [];
139
+ const next = cur.filter((r) => !roles.includes(r));
140
+ if (next.length === cur.length) return; // nothing removed
141
+ if (next.length) entry.roles[repo] = next; else delete entry.roles[repo];
142
+ writeJSON(hubPath, hub);
143
+ }
144
+
145
+ // Keep repos.json `domain_owners` in sync when a domain-owner role is granted/revoked via the roster,
146
+ // so the gate's per-repo reviewer-routing and the derivation fallback never drift from hub.json. Adds
147
+ // or removes the yad `name` and mirrors `domain_owner = domain_owners[0]`. No-op + warn if the repo is
148
+ // not registered. Returns true when the registry was written.
149
+ export function setRepoDomainOwners(root, repo, name, { add = true } = {}) {
150
+ const regPath = path.join(root, PROJECT_FILES.reposRegistry);
151
+ const registry = readJSON(regPath, { repos: [] });
152
+ const entry = (registry.repos || []).find((r) => r.name === repo);
153
+ if (!entry) { warn(`repo '${repo}' is not registered (.sdlc/repos.json) — domain_owners not synced`); return false; }
154
+ const owners = Array.isArray(entry.domain_owners) ? [...entry.domain_owners] : (entry.domain_owner ? [entry.domain_owner] : []);
155
+ const has = owners.includes(name);
156
+ let next;
157
+ if (add) { if (has) return false; next = [...owners, name]; }
158
+ else { if (!has) return false; next = owners.filter((o) => o !== name); }
159
+ entry.domain_owners = next;
160
+ entry.domain_owner = next[0] || '';
161
+ writeJSON(regPath, registry);
162
+ return true;
163
+ }
164
+
165
+ // Reconcile one repo's roles for a member to exactly `want`: grant what is new, revoke what is gone,
166
+ // and mirror domain-owner changes into repos.json. `current` is the member's existing roles for the
167
+ // repo. Shared by the `yad roster` walk and the `yad setup` per-repo role step. Idempotent.
168
+ export function reconcileRepoRoles(root, name, repo, current = [], want = []) {
169
+ const toAdd = want.filter((r) => !current.includes(r));
170
+ const toRemove = current.filter((r) => !want.includes(r));
171
+ if (!toAdd.length && !toRemove.length) { info(` ${repo}: unchanged`); return; }
172
+ if (toAdd.length) addRepoRoles(root, repo, Object.fromEntries(toAdd.map((r) => [r, [name]])));
173
+ if (toRemove.length) removeRepoRole(root, name, repo, toRemove);
174
+ if (toAdd.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: true });
175
+ if (toRemove.includes('domain-owner')) setRepoDomainOwners(root, repo, name, { add: false });
176
+ ok(` ${repo}: ${want.length ? want.join(', ') : 'cleared'}`);
177
+ }
178
+
71
179
  export function registerRepo(root, registry, { name, rpath, platform, domain_owner = '', domain_owners = null, default_branch = 'main', today = null }) {
72
180
  if (!insideRoot(root, rpath)) {
73
181
  warn(`${rpath} resolves outside the project root — skipped`);
@@ -370,6 +478,24 @@ export async function runSetup(root, opts = {}) {
370
478
  }
371
479
  }
372
480
 
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).
484
+ const hub7 = readJSON(hubPath, null);
485
+ if (registry.repos.length && hub7 && Array.isArray(hub7.roster) && hub7.roster.length
486
+ && await askYesNo('Assign/update roles for connected repos?', false)) {
487
+ for (const member of hub7.roster) {
488
+ if (!(await askYesNo(` edit ${member.name}'s repo roles?`, false))) continue;
489
+ for (const repo of registry.repos) {
490
+ const cur = rolesForScope(member, repo.name);
491
+ if (!(await askYesNo(` set ${member.name}'s role on ${repo.name}? (current: ${cur.length ? cur.join(', ') : 'none'})`, false))) continue;
492
+ const input = await ask(' roles (domain-owner/reviewer/owner, space-separated; blank = clear)', cur.join(' '));
493
+ const want = parseList(input).filter((x) => ['owner', 'reviewer', 'domain-owner'].includes(x));
494
+ reconcileRepoRoles(root, member.name, repo.name, cur, want);
495
+ }
496
+ }
497
+ }
498
+
373
499
  // 8. Wire each connected repo + the hub itself
374
500
  step(8, total, 'Wire connected repos + the hub (CI gates, PR template, comment scaffold, gate-sync)');
375
501
  if (registry.repos.length === 0) info('no repos to wire');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "2.6.0",
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 + 25 yad-* skills.",
3
+ "version": "2.8.0",
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 + 29 yad-* skills.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",
7
7
  "license": "MIT",
@@ -22,7 +22,6 @@
22
22
  "!cli/test.mjs",
23
23
  "!cli/test-checks.mjs",
24
24
  "skills/",
25
- "docs/index.html",
26
25
  "README.md",
27
26
  "LICENSE",
28
27
  "CHANGELOG.md"
@@ -55,7 +54,7 @@
55
54
  "deeptutor"
56
55
  ],
57
56
  "devDependencies": {
58
- "@eslint/js": "^9.39.4",
57
+ "@eslint/js": "^10.0.1",
59
58
  "@semantic-release/changelog": "^6.0.3",
60
59
  "eslint": "^9.39.4",
61
60
  "semantic-release": "^25.0.3"
@@ -176,6 +176,25 @@ learning:
176
176
  # tokens in the registry (kb name + sources are plain references, never credentials). Opt-in, never a gate.
177
177
  auth: user
178
178
 
179
+ # Interactive documentation (yad-connect-docs / yad-docs / yad-docs-overview / yad-docs-sync) — the
180
+ # generated React+Vite+Tailwind doc SITES. yad-docs builds a per-epic site (epics/EP-<slug>/docs-site/)
181
+ # from the authored artifacts, themed by the connected design system; yad-docs-overview builds the
182
+ # project SDLC-overview site (docs/sdlc-site/) from the pipeline definition, superseding docs/index.html.
183
+ # The deploy TARGET is a Pages host, auto-detected from hub.platform (github->github-pages,
184
+ # gitlab->gitlab-pages, null->build-only). The yad CLI does the npm build + Pages wiring + staleness;
185
+ # the content generation is the AI step. Docs are an OUTPUT ENRICHMENT — never a gate, never touch state.
186
+ docs:
187
+ registry: "{project-root}/.sdlc/docs.json" # project-wide docs/Pages connection (NOT per-epic)
188
+ targets: [github-pages, gitlab-pages] # supported Pages hosts; auto-detected from hub.platform
189
+ scope: hub # publish from: hub (default) | <repo-name> | dedicated
190
+ degrade: build-only # no Pages host / no gh|glab => build the dist, publish via CI
191
+ login_gate: false # public docs by default (the client-side gate is presentational only)
192
+ manifest: "{project-root}/epics/EP-<slug>/.sdlc/docs-build.json" # per-epic staleness baseline (yad-docs)
193
+ overview: "{project-root}/docs/sdlc-site/" # the project SDLC-overview site (yad-docs-overview)
194
+ # Auth: `yad-connect-docs` records only how to reach the Pages host (platform + base path); it stores NO
195
+ # tokens — publishing runs as the LOCAL user's own gh/glab or the platform CI. Idempotent, refreshable.
196
+ auth: user
197
+
179
198
  # Hub platform + front-half review bridge (yad-connect-repos `detect-hub`; yad-review-gate + yad-hub-bridge).
180
199
  # The product hub is itself a git repo on a platform. With the bridge enabled, the front-half review/
181
200
  # 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-test-cases yad-connect-repos yad-connect-design yad-connect-testing yad-connect-learning yad-learn yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-status)
14
+ SKILLS=(yad-analysis yad-epic yad-architecture yad-ui yad-stories yad-test-cases yad-connect-repos yad-connect-design yad-connect-testing yad-connect-learning yad-connect-docs yad-docs yad-docs-overview yad-docs-sync yad-learn yad-spec yad-implement yad-checks yad-pr-template yad-review-comments yad-hub-bridge yad-commit yad-open-pr yad-ship yad-engineer-review yad-backfill yad-run yad-review-gate yad-status)
15
15
 
16
16
  echo "Installing sdlc module from $ROOT/skills ..."
17
17
 
@@ -24,3 +24,7 @@ SDLC Workflow,yad-engineer-review,Engineer Review & Merge,ER,"Build-half Step E:
24
24
  SDLC Workflow,yad-backfill,Backfill Specs,BF,"Build-half Step G: generate specs for already-built features in an existing repo. Confirm Repomix (npx repomix CLI), pack ONE feature (compress + git logs, secret-scan), feed to AI with a 'describe what exists, do not invent' prompt, write a DRAFT spec marked verified: false. Human approval (reuse yad-review-gate) makes it real. Boundary auto-proposed and human-confirmed. A change is blocked only until the features it touches have approved specs. Never auto-advances.",,{repo: <repo>} {feature: <name + globs>} {action: pack|draft|approve|gate},3-build,,,false,demo-repos/<repo>/specs/backfill/<feature>/,spec.md backfill-check.sh
25
25
  SDLC Workflow,yad-run,Run (Automation),RN,"Phase 4 orchestrator: drive a story's back-half loop (spec→tasks→implement→checks) in one code repo, reading each step's automation dial from build-state — on machine_advance it advances on its own, on human_approve it stops for a human. Records every run in the trust log. Realizes Step B (a clean checks pass auto-advances to the engineer review when earned; any FAIL / scope overrun / contract touch HALTS). set-dial earns/reverts a step's automation (machine_advance gated by the trust threshold; front states and engineer-review refused); kill/unkill toggles the system-wide kill switch. Front states and the human merge gate never auto-advance.",,{epic: EP-<slug>} {story: EP-<slug>-S0N} {repo: <repo>} {action: run|set-dial|kill|unkill} {step: <back-step>} {to: human_approve|machine_advance},4-automate,yad-spec,yad-engineer-review,false,epics/EP-<slug>/.sdlc/,build-state/<story-id>.json trust-log.json
26
26
  SDLC Workflow,yad-status,SDLC Status,SS,"Read-only: print the full front-state chain, per-step dials, contract lock, story repo tags, and pending approvals at the active gate. For stories in the build half, also print each back-half step's automation dial and status, the trust record (runs / % approved-unchanged / earned vs gathering evidence), and the system-wide kill-switch state.",,{epic: EP-<slug>},1-front,,,false,,
27
+ SDLC Workflow,yad-connect-docs,Connect Docs Target,DX,"Setup/maintenance: connect a docs/Pages publishing target so the interactive-docs steps can deploy the generated site (not just commit its source). Auto-detects the platform from .sdlc/hub.json (github->github-pages, gitlab->gitlab-pages, null->build-only), resolves the Vite base path, and records the target in .sdlc/docs.json (local-user auth, no stored tokens). Degrades to build-only when gh/glab is absent. Idempotent and refreshable; one connection per project. Not a gated state — never touches epic state or approvals.",,{action: connect|refresh|list|disconnect} {target: github-pages|gitlab-pages|none} {scope: hub|<repo>|dedicated} {base_path: <override>},0-setup,,yad-docs,false,.sdlc/,docs.json
28
+ SDLC Workflow,yad-docs,Author Docs Site,DS,"Generate the per-epic interactive documentation SPA (animated flow canvas + role-based stakeholder doc pages) from the epic's approved artifacts (epic, architecture, locked contract, ui-design, stories, code-context, test-cases), themed by the connected design system, into epics/EP-<slug>/docs-site/. Copies the vendored React/Vite/Tailwind shell verbatim and generates src/data/*.ts deterministically; drives the yad docs build/deploy CLI to publish to Pages (or build-only). An OUTPUT ENRICHMENT — never a gate; never mutates state.json, approvals, or the contract lock.",,{epic: EP-<slug>} {action: generate|refresh|deploy} {login_gate: true|false},,yad-review-gate,,false,epics/EP-<slug>/docs-site/,docs-site/ docs-build.json
29
+ SDLC Workflow,yad-docs-overview,Docs Overview Site,DO,"Generate the project SDLC-overview interactive site (docs/sdlc-site/) — every stage from setup to ship modeled as flow paths, system components, and stakeholder roles — reusing the same vendored shell, themed with yadflow's brand palette. Reads config.yaml + module-help.csv + the overview diagram as the pipeline source. Folds the hand-maintained docs/index.html report into the site as report.html (linked from the nav). Not a gate — a project-level enrichment that regenerates whenever the skill set / pipeline changes.",,{action: generate|deploy},,,,false,docs/sdlc-site/,sdlc-site/ .docs-build.json
30
+ SDLC Workflow,yad-docs-sync,Docs Sync,DY,"Maintenance/CI: keep the generated doc sites fresh. Detect staleness (a content hash of the approved artifacts + the connected repos' HEAD shas + the doc-shell version vs each site's build manifest), report which sites drifted and why, regenerate + redeploy the stale ones, and wire a CI job that rebuilds on push (carrying [skip ci] + a concurrency group to prevent deploy loops). Generalizes the rule that feature work must hand-update docs/index.html + diagrams + skill counts. Refresh is always a human/CI decision; never a gate.",,{action: check|refresh|wire} {epic: EP-<slug>},,,,false,epics/EP-<slug>/.sdlc/,docs-build.json yad-docs.yml
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: yad-connect-docs
3
+ description: 'Connects a docs/Pages publishing target to the product hub so the interactive-docs steps can build and deploy the generated SPA — not just commit its source. Registers the target into the project-wide .sdlc/docs.json (GitHub Pages / GitLab Pages / build-only), auto-detecting the platform from .sdlc/hub.json and resolving the Vite base path, with local-user auth and no stored tokens. Detects whether gh/glab is present and degrades to build-only when absent. Run at setup or any time the publish target changes. Reusable, idempotent, refreshable. Use when the user says "connect docs", "connect Pages", "refresh the docs connection", or "list the docs connection".'
4
+ ---
5
+
6
+ # SDLC — Connect a Docs/Pages Target (make the docs steps publishable)
7
+
8
+ **Goal:** Let the interactive-docs steps (`yad-docs` per epic, `yad-docs-overview` project-wide)
9
+ **build and deploy** the generated React/Vite SPA to a real URL — a GitHub Pages or GitLab Pages site —
10
+ instead of only committing its source. This skill **connects** a publishing target to the product hub
11
+ and records *how* to reach it (the platform, the publish scope, the base path) — 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 docs registry. `yad-docs` / `yad-docs-overview`
15
+ consume it: when a target is connected, they theme + generate the site and drive `yad docs deploy`;
16
+ when nothing is connected (`target: "none"`), they still generate and **npm-build** the site but stop
17
+ at a local `dist/` — build-only, no publish, exactly as before.
18
+
19
+ ## Conventions
20
+
21
+ - `{project-root}` resolves from the project working directory (the **product hub**).
22
+ - The target is **GitHub-Pages-first but pluggable** — a *publish adapter*, mirroring the GitHub/GitLab
23
+ platform adapter the hub already uses. `github-pages` and `gitlab-pages` are the providers; `none` →
24
+ build-only (deliberate, no error).
25
+ - The platform CLI (`gh` / `glab`) is a **subprocess**, used read/deploy-only via the user's own auth —
26
+ never installed by this skill, never given a token. Absent ⇒ degrade to build-only (`source:
27
+ "unavailable"`).
28
+ - Registry: `{project-root}/.sdlc/docs.json` (project-wide, shared across all epics + the overview —
29
+ NOT per-epic), the sibling of `.sdlc/hub.json`, `.sdlc/repos.json`, and `.sdlc/design.json`.
30
+ - Per-epic / overview build manifests (`docs-build.json`) are written later by `yad-docs` /
31
+ `yad-docs-overview`, not here. This skill describes the *connection*; it does not build.
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
+ - `target` — `github-pages` | `gitlab-pages` | `none`. Default **auto-detected** from `.sdlc/hub.json`
38
+ `platform` (github → `github-pages`, gitlab → `gitlab-pages`, null/no hub → `none`).
39
+ - `scope` — `hub` (default) | `<repo-name>` | `dedicated`. Where the Pages site is published from (the
40
+ hub repo, one connected code repo, or a dedicated docs repo).
41
+ - `public` — `true` (default) | `false`. Whether the published site is public.
42
+ - `base_path` — optional explicit override of the Vite `base` (otherwise resolved, Step 2).
43
+
44
+ ## On Activation
45
+
46
+ ### Step 1 — Resolve the target + detect the platform (the publish adapter)
47
+ Determine the `target`. If not given, read `{project-root}/.sdlc/hub.json` `platform` and map it the same
48
+ way the hub bridge maps repos: `github` → `github-pages`, `gitlab` → `gitlab-pages`, `null`/no hub →
49
+ `none` (deliberate build-only). Reject a `target` value outside the three providers (fall back to the
50
+ detected default with a warning, the way `registerRepo` falls back on an unknown platform).
51
+
52
+ Then **probe the platform CLI** in the user's own session — `gh --version` / `gh auth status` for
53
+ GitHub, `glab --version` / `glab auth status` for GitLab. Record `source`:
54
+ - CLI present + authenticated → `source: "gh"` | `"glab"` (deploy can publish via the platform).
55
+ - CLI absent or unauthenticated → `source: "unavailable"` — record it and report that `yad-docs` will
56
+ **build-only** (npm build to a local `dist/`, no publish) until the CLI is available. No error — the
57
+ publish is purely additive, exactly like the `gh`/`glab` review bridge degrading.
58
+
59
+ **Auth is the local user's own** (`gh`/`glab`/git already on this device). The skill **stores no
60
+ tokens**; everything in the registry is a plain reference. Do **not** install a CLI as part of this step.
61
+
62
+ ### Step 2 — Decide the publish scope + resolve the base path
63
+ Resolve `scope` → `publishRepo`:
64
+ - `hub` (default) → publish from the hub repo (read its name from `hub.json` `git_url`).
65
+ - `<repo-name>` → publish from that connected code repo (must exist in `.sdlc/repos.json`).
66
+ - `dedicated` → a dedicated docs repo the user names (recorded as `publishRepo`).
67
+
68
+ Resolve `basePath` (the Vite `base`) per the table in `references/docs-registry.md`:
69
+ - **GitHub *project* Pages** (a repo that is not `<user>.github.io`) serve under `/<repo>/` → `basePath =
70
+ "/<repo>/"`. Per-epic sites nest under `/<repo>/epics/EP-<slug>/`; the overview under `/<repo>/`.
71
+ - **GitHub user/org Pages** (`<user>.github.io`) and **GitLab Pages** serve at the domain root → `basePath
72
+ = "/"`.
73
+ - An explicit `base_path` input always wins (recorded verbatim, normalized to a leading + trailing `/`).
74
+
75
+ ### Step 3 — Record the connection in the registry (idempotent)
76
+ Upsert into `{project-root}/.sdlc/docs.json` (create the file + parent `.sdlc/` if absent):
77
+
78
+ ```json
79
+ {
80
+ "target": "github-pages",
81
+ "scope": "hub",
82
+ "publishRepo": "<repo or hub name>",
83
+ "basePath": "/<repo>/",
84
+ "public": true,
85
+ "auth": "user",
86
+ "connectedAt": "<YYYY-MM-DD>",
87
+ "lastSyncedAt": "<YYYY-MM-DD>",
88
+ "source": "gh"
89
+ }
90
+ ```
91
+
92
+ - `target: "none"` records a deliberate build-only project: `{ "target": "none", "scope": "hub",
93
+ "publishRepo": null, "basePath": "/", "source": "unavailable", ... }`.
94
+ - `connect` is **idempotent** — re-running it overwrites the single connection in place (a project has
95
+ one docs target at a time; switching targets is just another `connect`).
96
+
97
+ ### Step 4 — Report (never auto-advance)
98
+ Report the connected `target`, its `scope`/`publishRepo`, the resolved `basePath`, whether the platform
99
+ CLI is available (or that the docs steps will degrade to **build-only**), and that **`yad-docs` /
100
+ `yad-docs-overview` will now build + deploy here**. Nothing auto-advances; this is setup. **Do not build
101
+ the site here — `yad-docs` builds.**
102
+
103
+ ## Other actions
104
+
105
+ - **`refresh`** — re-detect the platform CLI and re-resolve the base path (after the user authenticates a
106
+ session, renames the publish repo, or switches `scope`), updating `lastSyncedAt`. Same machinery as
107
+ `connect`. Re-detection may flip `source` between `gh`/`glab` and `unavailable` — report the change.
108
+ - **`list`** — print the current connection: `target`, `scope`/`publishRepo`, `basePath`, `public`, and a
109
+ **available/unavailable** flag for the platform CLI (best-effort, the user's own session). No target
110
+ connected ⇒ "build-only".
111
+ - **`disconnect`** — remove the registry file (or set `target: "none"`). The platform's own Pages site is
112
+ **never touched** — only the hub's record of it.
113
+
114
+ ## Hard rules
115
+
116
+ - **Local-user auth only; store no tokens.** Connect through the user's own `gh`/`glab`/git; never embed a
117
+ PAT or any credential in the registry. Everything recorded is a plain reference.
118
+ - **Degrade gracefully.** No target / no platform CLI → the docs steps **build-only** (local `dist/`) with
119
+ no error. Publishing is additive, never a blocker — the same discipline as the `gh`/`glab` review bridge.
120
+ - **Setup, not a gate.** Never touch `.sdlc/state.json`, approvals, or the contract lock from here.
121
+ - **Idempotent + refreshable.** `connect`/`refresh` are safe to re-run; a project carries one docs
122
+ connection at a time.
123
+ - **Describe the connection; do not build here.** This skill records *how to reach* the target.
124
+ `yad-docs` / `yad-docs-overview` generate, build, and deploy the site.
125
+
126
+ ## Reference
127
+ - Registry schema, the base-path resolution table, and the freshness/degrade rules:
128
+ `references/docs-registry.md`.
129
+ - The connect pattern this mirrors (design tool): `../yad-connect-design/SKILL.md`.
130
+ - The connect pattern this mirrors (code repos + hub detection): `../yad-connect-repos/SKILL.md`.
131
+ - The consumers — how `yad-docs` / `yad-docs-overview` build + deploy: `../yad-docs/SKILL.md`,
132
+ `../yad-docs-overview/SKILL.md`.
@@ -0,0 +1,74 @@
1
+ # `.sdlc/docs.json` — the docs/Pages registry
2
+
3
+ Project-wide, shared across every epic's docs site **and** the project overview site (NOT per-epic).
4
+ The sibling of `.sdlc/hub.json`, `.sdlc/repos.json`, and `.sdlc/design.json`. Written by
5
+ `yad-connect-docs`; read by `yad-docs`, `yad-docs-overview`, `yad-docs-sync`, and the `yad docs` CLI.
6
+ Holds **no credentials** — every field is a plain reference. Auth is always the local user's own
7
+ `gh`/`glab`/git session.
8
+
9
+ ## Schema
10
+
11
+ ```json
12
+ {
13
+ "target": "github-pages | gitlab-pages | none",
14
+ "scope": "hub | <repo-name> | dedicated",
15
+ "publishRepo": "<repo or hub name>",
16
+ "basePath": "/<repo>/",
17
+ "public": true,
18
+ "auth": "user",
19
+ "connectedAt": "<YYYY-MM-DD>",
20
+ "lastSyncedAt": "<YYYY-MM-DD>",
21
+ "source": "gh | glab | unavailable"
22
+ }
23
+ ```
24
+
25
+ | Field | Meaning |
26
+ |-------|---------|
27
+ | `target` | The publish adapter. `none` = deliberate build-only (no publish, no error). |
28
+ | `scope` | Where the Pages site publishes from: the `hub` repo, one connected `<repo-name>`, or a `dedicated` docs repo. |
29
+ | `publishRepo` | The concrete repo name resolved from `scope`. `null` when `target: "none"`. |
30
+ | `basePath` | The Vite `base` substituted into each generated site (resolution table below). Normalized to a leading + trailing `/`. |
31
+ | `public` | Whether the published site is public. |
32
+ | `auth` | Always `"user"` — local-user / platform-CLI session. No token is ever stored. |
33
+ | `connectedAt` / `lastSyncedAt` | ISO dates the connection was first written / last re-detected. |
34
+ | `source` | `gh`/`glab` when the platform CLI is present + authenticated (publish works); `unavailable` when absent (build-only). |
35
+
36
+ `target: "none"` records `{ "target": "none", "publishRepo": null, "basePath": "/", "source":
37
+ "unavailable", ... }`.
38
+
39
+ ## Platform auto-detection (from `.sdlc/hub.json`)
40
+
41
+ When `target` is not given, map the hub's `platform` the same way `yad-connect-repos` maps a repo host:
42
+
43
+ | `hub.json` `platform` | default `target` |
44
+ |-----------------------|------------------|
45
+ | `github` | `github-pages` |
46
+ | `gitlab` | `gitlab-pages` |
47
+ | `null` / no hub.json | `none` (build-only) |
48
+
49
+ ## Base-path resolution table
50
+
51
+ GitHub serves *project* Pages under a `/<repo>/` prefix, so Vite's `base` must match or every asset 404s.
52
+ User/org Pages and GitLab Pages serve at the domain root.
53
+
54
+ | Target + repo kind | `basePath` | Per-epic site URL | Overview site URL |
55
+ |--------------------|------------|-------------------|-------------------|
56
+ | GitHub **project** Pages (repo ≠ `<user>.github.io`) | `/<repo>/` | `/<repo>/epics/EP-<slug>/` | `/<repo>/` |
57
+ | GitHub **user/org** Pages (`<user>.github.io`) | `/` | `/epics/EP-<slug>/` | `/` |
58
+ | GitLab Pages | `/` | `/epics/EP-<slug>/` | `/` |
59
+ | explicit `base_path` input | as given (normalized) | nests `epics/EP-<slug>/` under it | the given base |
60
+
61
+ An explicit `base_path` input always wins. `yad-docs` substitutes `basePath` into the shell's Vite
62
+ config; per-epic sites append `epics/EP-<slug>/` so they nest under the overview without colliding.
63
+
64
+ ## Freshness + degrade rules
65
+
66
+ - **Freshness** here is connection-level, not content-level: `lastSyncedAt` reflects the last `refresh`.
67
+ *Site* staleness (artifacts/repos moved, shell upgraded) is tracked separately in each site's
68
+ `docs-build.json` and reconciled by `yad-docs-sync` — not here.
69
+ - **`list`** flags the platform CLI **available/unavailable** by probing `gh auth status` / `glab auth
70
+ status` in the user's own session (best-effort). A flip to `unavailable` means the docs steps
71
+ degrade to build-only until the CLI is back.
72
+ - **Degrade is silent + non-blocking.** No target / no CLI ⇒ `yad-docs` still generates + npm-builds the
73
+ site to a local `dist/`; only the publish step is skipped. The same discipline as the design-tool MCP
74
+ and the `gh`/`glab` review bridge being absent.
@@ -133,6 +133,10 @@ write only `{project-root}/.sdlc/hub.json` (`config.yaml` `hub.config`) — neve
133
133
  `roles[<repo>]` (and still **derived** as a fallback when a roster `name` equals a repo's
134
134
  `domain_owner`/`domain_owners` in `repos.json` — see `references/hub-config.md`). An unmapped login
135
135
  degrades to a plain `reviewer`, never auto-promoted to owner/domain-owner.
136
+ **The deterministic half is the `yad roster` CLI command** — runnable any time, not just at setup:
137
+ `yad roster list`; `yad roster add <login>` (upsert, then a repo-driven walk that asks for each
138
+ connected repo's role); `yad roster grant|revoke <name> <repo> <role>`; `yad roster remove <login>`.
139
+ A `domain-owner` grant/revoke keeps `repos.json` `domain_owners` in sync so the gate never drifts.
136
140
 
137
141
  If the hub has no remote (`platform: null`) or the bridge is disabled, the front-half gate runs
138
142
  file-only with no error — the bridge is purely additive.
@@ -33,7 +33,9 @@ login to an SDLC name + role. It is a single object for the hub itself — the s
33
33
 
34
34
  The roster is how a platform identity (a GitHub/GitLab **login**) becomes an SDLC **name + role(s)** in
35
35
  the file ledger (`approvals.json` / `comments.json`). Roles are the same three the gate uses:
36
- `owner | reviewer | domain-owner`.
36
+ `owner | reviewer | domain-owner`. Populate and edit it any time with the **`yad roster`** CLI command
37
+ (`list` / `add` / `grant` / `revoke` / `remove`) — `add` walks the connected repos asking for each
38
+ one's role, and a `domain-owner` grant keeps `repos.json` `domain_owners` in sync.
37
39
 
38
40
  - **`login`** — the platform username whose PR review / approval is being mapped.
39
41
  - **`name`** — the SDLC name written into the ledger (the same names used across `approvals.json`,