@design-ai/cli 4.55.0 → 4.56.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 (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +39 -0
  3. package/README.ko.md +7 -7
  4. package/README.md +9 -9
  5. package/cli/lib/mcp-server.mjs +208 -10
  6. package/cli/lib/site-analysis.mjs +297 -0
  7. package/cli/lib/site-args.mjs +433 -0
  8. package/cli/lib/site-bundle-build.mjs +127 -0
  9. package/cli/lib/site-bundle-check.mjs +454 -0
  10. package/cli/lib/site-bundle-commands.mjs +95 -0
  11. package/cli/lib/site-bundle-compare.mjs +157 -0
  12. package/cli/lib/site-bundle-contract.mjs +79 -0
  13. package/cli/lib/site-bundle-files.mjs +87 -0
  14. package/cli/lib/site-bundle-handoff-runbook-actions.mjs +113 -0
  15. package/cli/lib/site-bundle-handoff-runbook-evidence-fields.mjs +164 -0
  16. package/cli/lib/site-bundle-handoff-runbook-evidence.mjs +334 -0
  17. package/cli/lib/site-bundle-handoff-runbook-format.mjs +31 -0
  18. package/cli/lib/site-bundle-handoff-runbook-stage-metadata.mjs +84 -0
  19. package/cli/lib/site-bundle-handoff-runbook.mjs +1331 -0
  20. package/cli/lib/site-bundle-handoff-summary.mjs +183 -0
  21. package/cli/lib/site-bundle-handoff.mjs +271 -0
  22. package/cli/lib/site-bundle-readme.mjs +98 -0
  23. package/cli/lib/site-bundle-repair-report.mjs +143 -0
  24. package/cli/lib/site-bundle-repair.mjs +68 -0
  25. package/cli/lib/site-content.mjs +399 -0
  26. package/cli/lib/site-evidence.mjs +35 -0
  27. package/cli/lib/site-mcp-commands.mjs +28 -0
  28. package/cli/lib/site-mcp-probes.mjs +159 -0
  29. package/cli/lib/site-mcp-readiness.mjs +157 -0
  30. package/cli/lib/site-mcp-report.mjs +324 -0
  31. package/cli/lib/site-next-actions.mjs +333 -0
  32. package/cli/lib/site-options.mjs +104 -0
  33. package/cli/lib/site-prompts.mjs +332 -0
  34. package/cli/lib/site-starter.mjs +153 -0
  35. package/cli/lib/site-strings.mjs +23 -0
  36. package/cli/lib/site-tasks.mjs +93 -0
  37. package/cli/lib/site-workflow-graph.mjs +309 -0
  38. package/cli/lib/site-workspace.mjs +492 -0
  39. package/cli/lib/site.mjs +108 -6617
  40. package/docs/DISTRIBUTION.ko.md +35 -6
  41. package/docs/DISTRIBUTION.md +35 -8
  42. package/docs/RELEASE-CHECKLIST.md +20 -3
  43. package/docs/ROADMAP.md +2179 -0
  44. package/docs/external-status.md +22 -7
  45. package/docs/integrations/design-ai-mcp-server.md +32 -0
  46. package/docs/integrations/vscode-walkthrough.ko.md +3 -3
  47. package/docs/integrations/vscode-walkthrough.md +3 -3
  48. package/docs/site-overrides/main.html +1 -1
  49. package/package.json +1 -1
  50. package/tools/audit/package-smoke.py +106 -0
  51. package/tools/audit/registry-smoke.py +378 -10
  52. package/tools/audit/smoke_assertions.py +83 -22
@@ -0,0 +1,159 @@
1
+ // Read-only MCP probe helpers for Website Improvement workspaces.
2
+
3
+ import { existsSync, statSync } from "node:fs";
4
+
5
+ function parseHttpUrl(value) {
6
+ const raw = String(value || "").trim();
7
+ if (!raw) return null;
8
+ try {
9
+ const parsed = new URL(raw);
10
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
11
+ return parsed;
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+
17
+ function probeLevel({ passed, requestedStatus }) {
18
+ if (passed) return "pass";
19
+ return requestedStatus === "required" ? "fail" : "warn";
20
+ }
21
+
22
+ function probeStatus(items) {
23
+ if (items.some((item) => item.level === "fail")) return "fail";
24
+ if (items.some((item) => item.level === "warn")) return "warn";
25
+ return "pass";
26
+ }
27
+
28
+ function githubRepoSlug(repoUrl) {
29
+ const parsed = parseHttpUrl(repoUrl);
30
+ if (!parsed) return "";
31
+ const host = parsed.hostname.toLowerCase();
32
+ if (host !== "github.com" && !host.endsWith(".github.com")) return "";
33
+ const parts = parsed.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "").split("/");
34
+ if (parts.length < 2 || !parts[0] || !parts[1]) return "";
35
+ return `${parts[0]}/${parts[1]}`;
36
+ }
37
+
38
+ function figmaFileReference(figmaUrl) {
39
+ const parsed = parseHttpUrl(figmaUrl);
40
+ if (!parsed) return "";
41
+ const host = parsed.hostname.toLowerCase();
42
+ if (host !== "figma.com" && !host.endsWith(".figma.com")) return "";
43
+ const parts = parsed.pathname.replace(/^\/+|\/+$/g, "").split("/");
44
+ const supportedKinds = new Set(["design", "file", "board", "slides", "make"]);
45
+ if (parts.length < 2 || !supportedKinds.has(parts[0]) || !parts[1]) return "";
46
+ return `${parts[0]}/${parts[1]}`;
47
+ }
48
+
49
+ function pathExistsAsDirectory(localPath) {
50
+ const raw = String(localPath || "").trim();
51
+ if (!raw) return false;
52
+ try {
53
+ return existsSync(raw) && statSync(raw).isDirectory();
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ function buildProbeItem({ id, key, label, requestedStatus, passed, message, evidence = [], actions = [] }) {
60
+ const level = probeLevel({ passed, requestedStatus });
61
+ return {
62
+ id,
63
+ key,
64
+ label,
65
+ requestedStatus,
66
+ level,
67
+ passed,
68
+ message,
69
+ evidence,
70
+ actions: passed ? [] : actions,
71
+ };
72
+ }
73
+
74
+ function buildSiteMcpProbeItems(workspace) {
75
+ const profile = workspace.siteProfile;
76
+ const liveUrl = parseHttpUrl(profile.liveUrl);
77
+ const repoSlug = githubRepoSlug(profile.repoUrl);
78
+ const localRepoAvailable = pathExistsAsDirectory(profile.localPath);
79
+ const figmaRef = figmaFileReference(profile.figmaUrl);
80
+ const deployConfigured = profile.deployProvider !== "none";
81
+ const browserTargetReady = Boolean(liveUrl && profile.viewports.length > 0);
82
+
83
+ return [
84
+ buildProbeItem({
85
+ id: "github-repo-reference",
86
+ key: "github",
87
+ label: "GitHub repo reference",
88
+ requestedStatus: workspace.mcpReadiness.github,
89
+ passed: Boolean(repoSlug || localRepoAvailable),
90
+ message: repoSlug || localRepoAvailable
91
+ ? "Target repo reference is parseable for Codex handoff."
92
+ : "Target repo reference is not probe-ready.",
93
+ evidence: [
94
+ repoSlug ? `github repo: ${repoSlug}` : "",
95
+ localRepoAvailable ? `localPath exists: ${profile.localPath}` : "",
96
+ ].filter(Boolean),
97
+ actions: ["Add a github.com owner/repo URL or an existing local repo path before implementation handoff."],
98
+ }),
99
+ buildProbeItem({
100
+ id: "figma-url-reference",
101
+ key: "figma",
102
+ label: "Figma file reference",
103
+ requestedStatus: workspace.mcpReadiness.figma,
104
+ passed: Boolean(figmaRef),
105
+ message: figmaRef
106
+ ? "Figma URL is parseable for design-context handoff."
107
+ : "Figma URL is missing or not parseable.",
108
+ evidence: figmaRef ? [`figma reference: ${figmaRef}`] : [],
109
+ actions: ["Add a figma.com design/file/board/slides/make URL or mark Figma unused."],
110
+ }),
111
+ buildProbeItem({
112
+ id: "browser-smoke-target",
113
+ key: "browser",
114
+ label: "Browser smoke target",
115
+ requestedStatus: workspace.mcpReadiness.browser,
116
+ passed: browserTargetReady,
117
+ message: browserTargetReady
118
+ ? "Browser smoke target and viewport set are ready for manual or MCP-driven QA."
119
+ : "Browser smoke target is incomplete.",
120
+ evidence: [
121
+ liveUrl ? `liveUrl host: ${liveUrl.hostname}` : "",
122
+ profile.viewports.length ? `viewports: ${profile.viewports.join(", ")}` : "",
123
+ ].filter(Boolean),
124
+ actions: ["Add a valid http(s) liveUrl and at least one viewport before Browser/Playwright QA."],
125
+ }),
126
+ buildProbeItem({
127
+ id: "deploy-provider-reference",
128
+ key: "deploy",
129
+ label: "Deployment provider reference",
130
+ requestedStatus: workspace.mcpReadiness.deploy,
131
+ passed: Boolean(deployConfigured && liveUrl),
132
+ message: deployConfigured && liveUrl
133
+ ? "Deployment provider and live URL are configured for verification handoff."
134
+ : "Deployment provider or live URL is not configured.",
135
+ evidence: [
136
+ `deployProvider: ${profile.deployProvider}`,
137
+ liveUrl ? `liveUrl host: ${liveUrl.hostname}` : "",
138
+ ].filter(Boolean),
139
+ actions: ["Set siteProfile.deployProvider and liveUrl before deployment verification."],
140
+ }),
141
+ ];
142
+ }
143
+
144
+ export function buildSiteMcpProbeReport(workspace) {
145
+ const items = buildSiteMcpProbeItems(workspace)
146
+ .filter((item) => item.requestedStatus !== "unused" && item.requestedStatus !== "unavailable");
147
+ const status = probeStatus(items);
148
+ return {
149
+ enabled: true,
150
+ mode: "read-only-local",
151
+ externalCalls: false,
152
+ status,
153
+ count: items.length,
154
+ pass: items.filter((item) => item.level === "pass").length,
155
+ warn: items.filter((item) => item.level === "warn").length,
156
+ fail: items.filter((item) => item.level === "fail").length,
157
+ items,
158
+ };
159
+ }
@@ -0,0 +1,157 @@
1
+ // MCP readiness scoring helpers for Website Improvement workspaces.
2
+
3
+ function isLikelyHttpUrl(value) {
4
+ const raw = String(value || "").trim();
5
+ if (!raw) return false;
6
+ try {
7
+ const parsed = new URL(raw);
8
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ function mcpReadinessEvidence(workspace, key) {
15
+ const profile = workspace.siteProfile;
16
+ const hasRepo = Boolean(profile.repoUrl || profile.localPath);
17
+ const hasLiveUrl = isLikelyHttpUrl(profile.liveUrl);
18
+
19
+ const map = {
20
+ github: {
21
+ ready: hasRepo,
22
+ evidence: [
23
+ profile.repoUrl ? `repoUrl: ${profile.repoUrl}` : "",
24
+ profile.localPath ? `localPath: ${profile.localPath}` : "",
25
+ ].filter(Boolean),
26
+ actions: ["Add siteProfile.repoUrl or siteProfile.localPath before Codex implementation handoff."],
27
+ },
28
+ figma: {
29
+ ready: Boolean(profile.figmaUrl),
30
+ evidence: profile.figmaUrl ? [`figmaUrl: ${profile.figmaUrl}`] : [],
31
+ actions: ["Add siteProfile.figmaUrl or mark Figma unused for this site."],
32
+ },
33
+ browser: {
34
+ ready: hasLiveUrl && profile.viewports.length > 0,
35
+ evidence: [
36
+ hasLiveUrl ? `liveUrl: ${profile.liveUrl}` : "",
37
+ profile.viewports.length ? `viewports: ${profile.viewports.join(", ")}` : "",
38
+ ].filter(Boolean),
39
+ actions: ["Add a valid siteProfile.liveUrl and at least one viewport for Browser/Playwright QA."],
40
+ },
41
+ chromeDevtools: {
42
+ ready: hasLiveUrl,
43
+ evidence: hasLiveUrl ? [`liveUrl: ${profile.liveUrl}`] : [],
44
+ actions: ["Add a valid siteProfile.liveUrl before Chrome DevTools debugging."],
45
+ },
46
+ deploy: {
47
+ ready: profile.deployProvider !== "none" && hasLiveUrl,
48
+ evidence: [
49
+ `deployProvider: ${profile.deployProvider}`,
50
+ hasLiveUrl ? `liveUrl: ${profile.liveUrl}` : "",
51
+ ].filter(Boolean),
52
+ actions: ["Set siteProfile.deployProvider and liveUrl before deployment verification."],
53
+ },
54
+ sentry: {
55
+ ready: Boolean(profile.sentryProject),
56
+ evidence: profile.sentryProject ? [`sentryProject: ${profile.sentryProject}`] : [],
57
+ actions: ["Add siteProfile.sentryProject or mark Sentry unused until production errors are in scope."],
58
+ },
59
+ database: {
60
+ ready: profile.database !== "none",
61
+ evidence: [`database: ${profile.database}`],
62
+ actions: ["Set siteProfile.database to supabase, neon, postgres, or other when DB access is required."],
63
+ },
64
+ cms: {
65
+ ready: profile.cms !== "none",
66
+ evidence: [`cms: ${profile.cms}`],
67
+ actions: ["Set siteProfile.cms to sanity, contentful, wordpress, shopify, or other when content access is required."],
68
+ },
69
+ collaboration: {
70
+ ready: false,
71
+ evidence: [],
72
+ actions: ["Keep Collaboration optional/unused, or record the active Notion/Slack/Linear/Jira destination in reportNotes for handoff."],
73
+ },
74
+ research: {
75
+ ready: hasLiveUrl,
76
+ evidence: hasLiveUrl ? [`liveUrl: ${profile.liveUrl}`] : [],
77
+ actions: ["Add siteProfile.liveUrl before competitor or external research prompts."],
78
+ },
79
+ };
80
+
81
+ return map[key] || {
82
+ ready: false,
83
+ evidence: [],
84
+ actions: [`Add readiness evidence for ${key}.`],
85
+ };
86
+ }
87
+
88
+ export function mcpItemReport(workspace, key, label) {
89
+ const requestedStatus = workspace.mcpReadiness[key];
90
+ const check = mcpReadinessEvidence(workspace, key);
91
+
92
+ if (requestedStatus === "unused") {
93
+ return {
94
+ key,
95
+ label,
96
+ requestedStatus,
97
+ state: "unused",
98
+ level: "pass",
99
+ evidence: ["Marked unused in mcpReadiness."],
100
+ actions: [],
101
+ };
102
+ }
103
+
104
+ if (requestedStatus === "unavailable") {
105
+ return {
106
+ key,
107
+ label,
108
+ requestedStatus,
109
+ state: "unavailable",
110
+ level: "pass",
111
+ evidence: ["Marked unavailable in mcpReadiness; generated prompts should not assume this MCP."],
112
+ actions: [],
113
+ };
114
+ }
115
+
116
+ if (key === "collaboration" && requestedStatus === "optional") {
117
+ return {
118
+ key,
119
+ label,
120
+ requestedStatus,
121
+ state: "ready",
122
+ level: "pass",
123
+ evidence: ["Optional collaboration is tracked in handoff notes for this local MVP."],
124
+ actions: [],
125
+ };
126
+ }
127
+
128
+ if (check.ready) {
129
+ return {
130
+ key,
131
+ label,
132
+ requestedStatus,
133
+ state: "ready",
134
+ level: "pass",
135
+ evidence: check.evidence,
136
+ actions: [],
137
+ };
138
+ }
139
+
140
+ return {
141
+ key,
142
+ label,
143
+ requestedStatus,
144
+ state: "missing",
145
+ level: requestedStatus === "required" ? "fail" : "warn",
146
+ evidence: check.evidence,
147
+ actions: check.actions,
148
+ };
149
+ }
150
+
151
+ export function siteMcpCheckStatus(items, taskGaps, workspaceIssues) {
152
+ if (workspaceIssues.some((issue) => issue.level === "fail")) return "fail";
153
+ if (items.some((item) => item.level === "fail")) return "fail";
154
+ if (workspaceIssues.some((issue) => issue.level === "warn")) return "warn";
155
+ if (items.some((item) => item.level === "warn") || taskGaps.length > 0) return "warn";
156
+ return "pass";
157
+ }
@@ -0,0 +1,324 @@
1
+ // MCP readiness reports and action plans for Website Improvement workspaces.
2
+
3
+ import { buildSiteMcpProbeCommandSet, siteMcpCommandTarget } from "./site-mcp-commands.mjs";
4
+ import { buildSiteMcpProbeReport } from "./site-mcp-probes.mjs";
5
+ import { mcpItemReport, siteMcpCheckStatus } from "./site-mcp-readiness.mjs";
6
+ import { MCP_ITEMS, PRIORITY_OPTIONS } from "./site-options.mjs";
7
+ import { markdownList, markdownTable, normalizeStringArray } from "./site-strings.mjs";
8
+
9
+ export function combineStatuses(...statuses) {
10
+ if (statuses.includes("fail")) return "fail";
11
+ if (statuses.includes("warn")) return "warn";
12
+ return "pass";
13
+ }
14
+
15
+ export function normalizeMcpKey(value) {
16
+ const raw = String(value || "").trim();
17
+ if (!raw) return "";
18
+ const canonical = {
19
+ chrome: "chromeDevtools",
20
+ chromeDevTools: "chromeDevtools",
21
+ devtools: "chromeDevtools",
22
+ playwright: "browser",
23
+ browserplaywright: "browser",
24
+ github: "github",
25
+ figma: "figma",
26
+ browser: "browser",
27
+ chromeDevtools: "chromeDevtools",
28
+ deploy: "deploy",
29
+ sentry: "sentry",
30
+ database: "database",
31
+ cms: "cms",
32
+ collaboration: "collaboration",
33
+ research: "research",
34
+ };
35
+ return canonical[raw] || canonical[raw.replace(/[^a-zA-Z]/g, "")] || raw;
36
+ }
37
+
38
+ function mcpTaskGaps(workspace) {
39
+ return workspace.refactorTasks.flatMap((task) => normalizeStringArray(task.recommendedMcp).flatMap((rawMcp) => {
40
+ const key = normalizeMcpKey(rawMcp);
41
+ if (!key || !workspace.mcpReadiness[key]) return [];
42
+ const status = workspace.mcpReadiness[key];
43
+ if (status !== "unused" && status !== "unavailable") return [];
44
+ return [{
45
+ taskId: task.id,
46
+ title: task.title,
47
+ mcp: key,
48
+ status,
49
+ level: "warn",
50
+ message: `Task '${task.title}' recommends ${key}, but mcpReadiness marks it ${status}.`,
51
+ }];
52
+ }));
53
+ }
54
+
55
+ export function buildSiteMcpCheckReport(workspace, summary = {}, options = {}) {
56
+ const items = MCP_ITEMS.map(([key, label]) => mcpItemReport(workspace, key, label));
57
+ const taskGaps = mcpTaskGaps(workspace);
58
+ const workspaceIssues = (summary.issues || []).filter((issue) => issue.level !== "pass");
59
+ const baseStatus = siteMcpCheckStatus(items, taskGaps, workspaceIssues);
60
+ const probes = options.probes ? buildSiteMcpProbeReport(workspace) : null;
61
+ const status = probes ? combineStatuses(baseStatus, probes.status) : baseStatus;
62
+ const nextActions = [
63
+ ...items.flatMap((item) => item.actions),
64
+ ...(probes ? probes.items.flatMap((item) => item.actions.map((action) => `Probe ${item.id}: ${action}`)) : []),
65
+ ...taskGaps.map((gap) => `Align task '${gap.taskId}' recommendedMcp with mcpReadiness.${gap.mcp}.`),
66
+ ];
67
+
68
+ const report = {
69
+ filePath: summary.filePath || "workspace.json",
70
+ status,
71
+ workspaceStatus: summary.status || "unknown",
72
+ site: {
73
+ name: workspace.siteProfile.name,
74
+ liveUrl: workspace.siteProfile.liveUrl,
75
+ repoUrl: workspace.siteProfile.repoUrl,
76
+ localPath: workspace.siteProfile.localPath,
77
+ },
78
+ counts: {
79
+ total: items.length,
80
+ required: items.filter((item) => item.requestedStatus === "required").length,
81
+ optional: items.filter((item) => item.requestedStatus === "optional").length,
82
+ ready: items.filter((item) => item.state === "ready").length,
83
+ missing: items.filter((item) => item.state === "missing").length,
84
+ unused: items.filter((item) => item.state === "unused").length,
85
+ unavailable: items.filter((item) => item.state === "unavailable").length,
86
+ taskGaps: taskGaps.length,
87
+ },
88
+ items,
89
+ taskGaps,
90
+ workspaceIssues,
91
+ nextActions,
92
+ };
93
+ if (probes) {
94
+ report.probes = probes;
95
+ report.commands = buildSiteMcpProbeCommandSet(siteMcpCommandTarget(report.filePath));
96
+ }
97
+ return report;
98
+ }
99
+
100
+ export function formatSiteMcpCheckJson(report) {
101
+ return JSON.stringify(report, null, 2);
102
+ }
103
+
104
+ export function formatSiteMcpCheckHuman(report) {
105
+ return [
106
+ `Website Improvement MCP readiness: ${report.site.name}`,
107
+ "",
108
+ `Status: ${report.status}`,
109
+ `Workspace status: ${report.workspaceStatus}`,
110
+ `Required MCP: ${report.counts.required}`,
111
+ `Ready: ${report.counts.ready}`,
112
+ `Missing: ${report.counts.missing}`,
113
+ `Task gaps: ${report.counts.taskGaps}`,
114
+ "",
115
+ "MCP checks:",
116
+ ...report.items.map((item) => {
117
+ const evidence = item.evidence.length ? item.evidence.join("; ") : "no evidence";
118
+ const action = item.actions.length ? `\n Next: ${item.actions.join(" ")}` : "";
119
+ return `- [${item.level}] ${item.label} (${item.requestedStatus}) -> ${item.state}\n Evidence: ${evidence}${action}`;
120
+ }),
121
+ "",
122
+ "Task MCP gaps:",
123
+ ...(report.taskGaps.length
124
+ ? report.taskGaps.map((gap) => `- [${gap.level}] ${gap.taskId}: ${gap.message}`)
125
+ : ["- none"]),
126
+ ...(report.probes ? [
127
+ "",
128
+ "Read-only probes:",
129
+ `Mode: ${report.probes.mode}; external calls: ${report.probes.externalCalls ? "yes" : "no"}; status: ${report.probes.status}`,
130
+ ...report.probes.items.map((item) => {
131
+ const evidence = item.evidence.length ? item.evidence.join("; ") : "no evidence";
132
+ const action = item.actions.length ? `\n Next: ${item.actions.join(" ")}` : "";
133
+ return `- [${item.level}] ${item.label} (${item.requestedStatus}) -> ${item.passed ? "pass" : "needs attention"}\n Evidence: ${evidence}${action}`;
134
+ }),
135
+ ] : []),
136
+ ...(report.commands ? [
137
+ "",
138
+ "Probe commands:",
139
+ `- Save readiness probe report: \`${report.commands.mcpCheckProbesHumanOut}\``,
140
+ `- Save readiness probe JSON: \`${report.commands.mcpCheckProbesJsonOut}\``,
141
+ `- Generate probe action plan JSON: \`${report.commands.mcpPlanProbesJson}\``,
142
+ `- Save probe action plan JSON: \`${report.commands.mcpPlanProbesJsonOut}\``,
143
+ ] : []),
144
+ "",
145
+ "Next actions:",
146
+ ...(report.nextActions.length ? report.nextActions.map((action) => `- ${action}`) : ["- none"]),
147
+ ].join("\n");
148
+ }
149
+
150
+ function mcpActionPlanTaskRows(workspace, report) {
151
+ const stateByKey = new Map(report.items.map((item) => [item.key, item.state]));
152
+ const topTasks = workspace.refactorTasks
153
+ .slice()
154
+ .sort((a, b) => PRIORITY_OPTIONS.indexOf(a.priority) - PRIORITY_OPTIONS.indexOf(b.priority))
155
+ .slice(0, 8);
156
+
157
+ if (topTasks.length === 0) {
158
+ return [["No refactor tasks", "n/a", "n/a", "Generate starter tasks with `design-ai site <workspace.json> --tasks`."]];
159
+ }
160
+
161
+ return topTasks.map((task) => {
162
+ const mcps = normalizeStringArray(task.recommendedMcp);
163
+ const states = mcps.length
164
+ ? mcps.map((rawMcp) => {
165
+ const key = normalizeMcpKey(rawMcp);
166
+ return `${key}: ${stateByKey.get(key) || "unknown"}`;
167
+ }).join(", ")
168
+ : "none";
169
+ return [
170
+ task.id,
171
+ `${task.priority} / ${task.impact}`,
172
+ mcps.join(", ") || "none",
173
+ states,
174
+ ];
175
+ });
176
+ }
177
+
178
+ export function buildSiteMcpActionPlanData(workspace, summary = {}, options = {}) {
179
+ const report = buildSiteMcpCheckReport(workspace, summary, options);
180
+ const filePath = report.filePath || "workspace.json";
181
+ const commandTarget = siteMcpCommandTarget(filePath);
182
+ const probeCommands = buildSiteMcpProbeCommandSet(commandTarget);
183
+ const requiredGaps = report.items.filter((item) => item.requestedStatus === "required" && item.level !== "pass");
184
+ const optionalGaps = report.items.filter((item) => item.requestedStatus === "optional" && item.level !== "pass");
185
+ const blockingIssues = [
186
+ ...report.workspaceIssues.filter((issue) => issue.level === "fail").map((issue) => `${issue.id}: ${issue.message}`),
187
+ ...requiredGaps.map((item) => `${item.label}: ${item.actions.join(" ") || "Add required readiness evidence."}`),
188
+ ];
189
+ const warningIssues = [
190
+ ...report.workspaceIssues.filter((issue) => issue.level === "warn").map((issue) => `${issue.id}: ${issue.message}`),
191
+ ...optionalGaps.map((item) => `${item.label}: ${item.actions.join(" ") || "Add optional readiness evidence or mark unused."}`),
192
+ ...report.taskGaps.map((gap) => `${gap.taskId}: ${gap.message}`),
193
+ ];
194
+ const taskAlignment = mcpActionPlanTaskRows(workspace, report).map((row) => ({
195
+ task: row[0],
196
+ priorityImpact: row[1],
197
+ recommendedMcp: row[2],
198
+ readinessState: row[3],
199
+ }));
200
+ const commands = {
201
+ mcpCheck: `design-ai site ${commandTarget} --mcp-check --strict --json`,
202
+ mcpCheckProbesHumanOut: probeCommands.mcpCheckProbesHumanOut,
203
+ mcpCheckProbesJsonOut: probeCommands.mcpCheckProbesJsonOut,
204
+ mcpPlanProbesJsonOut: probeCommands.mcpPlanProbesJsonOut,
205
+ tasks: `design-ai site ${commandTarget} --tasks --out website-workspace.tasks.json`,
206
+ implementationPrompt: `design-ai site ${commandTarget} --prompt codex-implementation --task 1 --out codex-implementation.md`,
207
+ handoffReport: `design-ai site ${commandTarget} --report --out website-handoff.md`,
208
+ };
209
+ const executionSequence = [
210
+ "Fix every blocking item before target-repo implementation handoff.",
211
+ "Resolve warnings that affect the next selected refactor task, or mark the MCP unused when it is intentionally out of scope.",
212
+ "Re-run the strict readiness gate and keep the JSON output with the handoff package.",
213
+ "Generate or refresh starter tasks, then export the selected Codex implementation prompt.",
214
+ "Run target-repo lint/typecheck/build plus desktop, tablet, mobile, keyboard, and screen-reader verification after implementation.",
215
+ ];
216
+ const boundaries = [
217
+ "This plan is deterministic and local.",
218
+ "It does not call external MCPs, mutate the target website repo, run Lighthouse/axe, capture screenshots, or write to deployment/CMS/Sentry systems.",
219
+ "Run the generated Codex/Claude prompts in the target website workflow after this readiness plan is clean.",
220
+ ];
221
+
222
+ return {
223
+ kind: "website-improvement-mcp-action-plan",
224
+ version: 1,
225
+ filePath,
226
+ status: report.status,
227
+ workspaceStatus: report.workspaceStatus,
228
+ site: report.site,
229
+ counts: report.counts,
230
+ readinessMatrix: report.items,
231
+ probes: report.probes || null,
232
+ blockingItems: blockingIssues,
233
+ warnings: warningIssues,
234
+ taskAlignment,
235
+ taskGaps: report.taskGaps,
236
+ workspaceIssues: report.workspaceIssues,
237
+ nextActions: report.nextActions,
238
+ executionSequence,
239
+ commands,
240
+ boundaries,
241
+ externalCalls: false,
242
+ targetRepoMutation: false,
243
+ };
244
+ }
245
+
246
+ export function formatSiteMcpActionPlanJson(plan) {
247
+ return JSON.stringify(plan, null, 2);
248
+ }
249
+
250
+ export function buildSiteMcpActionPlan(workspace, summary = {}, options = {}) {
251
+ const plan = buildSiteMcpActionPlanData(workspace, summary, options);
252
+
253
+ return [
254
+ `# Website improvement MCP action plan: ${plan.site.name}`,
255
+ "",
256
+ "## Summary",
257
+ `- Source: ${plan.filePath}`,
258
+ `- Status: ${plan.status}`,
259
+ `- Workspace status: ${plan.workspaceStatus}`,
260
+ `- Live URL: ${plan.site.liveUrl || "not provided"}`,
261
+ `- Repo: ${plan.site.repoUrl || plan.site.localPath || "not provided"}`,
262
+ `- Ready MCP: ${plan.counts.ready}/${plan.counts.total}`,
263
+ `- Missing MCP: ${plan.counts.missing}`,
264
+ `- Task/MCP gaps: ${plan.counts.taskGaps}`,
265
+ "",
266
+ "## Readiness Matrix",
267
+ markdownTable(
268
+ ["MCP", "Requested", "State", "Level", "Evidence"],
269
+ plan.readinessMatrix.map((item) => [
270
+ item.label,
271
+ item.requestedStatus,
272
+ item.state,
273
+ item.level,
274
+ item.evidence.length ? item.evidence.join("; ") : "none",
275
+ ]),
276
+ ),
277
+ ...(plan.probes ? [
278
+ "",
279
+ "## Read-Only Probes",
280
+ "",
281
+ `- Probe status: ${plan.probes.status}`,
282
+ `- Mode: ${plan.probes.mode}`,
283
+ `- External calls: ${plan.probes.externalCalls ? "yes" : "no"}`,
284
+ "",
285
+ markdownTable(
286
+ ["Probe", "MCP", "Level", "Result", "Evidence", "Next Action"],
287
+ plan.probes.items.map((item) => [
288
+ item.label,
289
+ item.key,
290
+ item.level,
291
+ item.passed ? "pass" : "needs attention",
292
+ item.evidence.length ? item.evidence.join("; ") : "none",
293
+ item.actions.length ? item.actions.join(" ") : "none",
294
+ ]),
295
+ ),
296
+ ] : []),
297
+ "",
298
+ "## Blocking Items",
299
+ markdownList(plan.blockingItems, "No blocking readiness issues."),
300
+ "",
301
+ "## Warnings",
302
+ markdownList(plan.warnings, "No optional readiness or task/MCP warnings."),
303
+ "",
304
+ "## Task/MCP Alignment",
305
+ markdownTable(
306
+ ["Task", "Priority / impact", "Recommended MCP", "Readiness state"],
307
+ plan.taskAlignment.map((item) => [
308
+ item.task,
309
+ item.priorityImpact,
310
+ item.recommendedMcp,
311
+ item.readinessState,
312
+ ]),
313
+ ),
314
+ "",
315
+ "## Execution Sequence",
316
+ ...plan.executionSequence.map((item, index) => `${index + 1}. ${item}`),
317
+ "",
318
+ "## Commands",
319
+ ...Object.values(plan.commands).map((command) => `- \`${command}\``),
320
+ "",
321
+ "## Boundaries",
322
+ ...plan.boundaries.map((boundary) => `- ${boundary}`),
323
+ ].join("\n");
324
+ }